关于java:手把手教你集成Google-Authenticator实现多因素认证MFA

9次阅读

共计 10277 个字符,预计需要花费 26 分钟才能阅读完成。

近年来层出不穷的安全漏洞表明,单单只通过明码不能保障真正的平安,为了进一步加强身份认证的过程本文在校验用户名和明码之后再减少一层基于 Goole Authenticator 的认证来进步零碎的安全性。
操作形式:
1、手机利用商店下载 Google Authenticator
2、调用 LoginService 的 generateGoogleAuthQRCode 办法生成二维码

3、应用手机关上 Google Authenticator 扫描该二维码进行绑定,绑定后会每隔 30 秒生成一次 6 位动静验证码

4、调用 LoginService 的 login 办法进行登录 (需传入手机上显示的 6 位动静验证码,否则校验不被通过)
以下分享一次 Spring Boot 集成 Goole Authenticator 的案例关键性代码

@Service
public class LoginServiceImpl implements LoginService {

    private final UserService userService;

    public LoginServiceImpl(UserService userService) {this.userService = userService;}

    /**
     * 登录接口
     * @param loginParam {"username":"用户名", "password":"明码", "mfaCode":"手机利用 Google Authenticator 生成的验证码"}
     * @param servletRequest
     * @return
     */
    @Override
    public UserVo login(LoginParam loginParam, HttpServletRequest servletRequest) {
        // 校验用户名和明码是否匹配
        User user = getUserWithValidatePass(loginParam.getUsername(), loginParam.getPassword());
        // 验证数据库保留的密钥和输出的验证码是否匹配
        boolean verification = GoogleAuthenticatorUtils.verification(user.getGoogleAuthenticatorSecret(), loginParam.getMfaCode());
        if (!verification) {throw new BadRequestException("验证码校验失败");
        }
        // 用户信息保留到 session 中
        servletRequest.getSession().setAttribute("user", user);
        UserVo userVo = new UserVo();
        BeanUtils.copyProperties(user, userVo);
        return userVo;
    }

    /**
     * 生成二维码的 Base64 编码
     * 能够应用手机利用 Google Authenticator 来扫描二维码进行绑定
     * @param username
     * @param password
     * @return
     */
    @Override
    public String generateGoogleAuthQRCode(String username, String password) {
        // 校验用户名和明码是否匹配
        User user = getUserWithValidatePass(username, password);
        String secretKey;
        if (StringUtils.isEmpty(user.getGoogleAuthenticatorSecret())) {secretKey = GoogleAuthenticatorUtils.createSecretKey();
        }else {secretKey = user.getGoogleAuthenticatorSecret();
        }
        // 生成二维码
        String qrStr;
        try(ByteArrayOutputStream bos = new ByteArrayOutputStream()){String keyUri = GoogleAuthenticatorUtils.createKeyUri(secretKey, username, "Demo_System");  // Demo_System 服务标识不参加运算,可任意设置
            QRCodeUtils.writeToStream(keyUri, bos);
            qrStr = Base64.encodeBase64String(bos.toByteArray());
        }catch (WriterException | IOException e) {throw new ServiceException("生成二维码失败", e);
        }
        if (StringUtils.isEmpty(qrStr)) {throw new ServiceException("生成二维码失败");
        }
        user.setGoogleAuthenticatorSecret(secretKey);
        userService.updateById(user);
        return "data:image/png;base64," + qrStr;
    }


    private User getUserWithValidatePass(String username, String password) {
        String mismatchTip = "用户名或者明码不正确";
        // 依据用户名查问用户信息
        User user = userService.getByUsername(username)
                .orElseThrow(() -> new BadRequestException(mismatchTip));
        // 比对明码是否正确
        String encryptPassword = SecureUtil.md5(password);
        if (!encryptPassword.equals(user.getPassword())) {throw new BadRequestException(mismatchTip);
        }
        return user;
    }
}

GoogleAuthenticatorUtils 提供了生成密钥、校验验证码是否和密钥匹配等性能

public class GoogleAuthenticatorUtils {

    /**
     * 工夫前后偏移量
     * 用于避免客户端工夫不准确导致生成的 TOTP 与服务器端的 TOTP 始终不统一
     * 如果为 0, 以后工夫为 10:10:15
     * 则表明在 10:10:00-10:10:30 之间生成的 TOTP 能校验通过
     * 如果为 1, 则表明在
     * 10:09:30-10:10:00
     * 10:10:00-10:10:30
     * 10:10:30-10:11:00 之间生成的 TOTP 能校验通过
     * 以此类推
     */
    private static final int TIME_OFFSET = 0;

    /**
     * 创立密钥
     */
    public static String createSecretKey() {SecureRandom random = new SecureRandom();
        byte[] bytes = new byte[20];
        random.nextBytes(bytes);
        return Base32.encode(bytes).toLowerCase();}

    /**
     * 依据密钥获取验证码
     * 返回字符串是因为数值有可能以 0 结尾
     * @param secretKey 密钥
     * @param time 第几个 30 秒 System.currentTimeMillis() / 1000 / 30
     */
    public static String generateTOTP(String secretKey, long time) {byte[] bytes =  Base32.decode(secretKey.toUpperCase());
        String hexKey =HexUtil.encodeHexStr(bytes);
        String hexTime = Long.toHexString(time);
        return TOTP.generateTOTP(hexKey, hexTime, "6");
    }

    /**
     * 生成 Google Authenticator Key Uri
     * Google Authenticator 规定的 Key Uri 格局: otpauth://totp/{issuer}:{account}?secret={secret}&issuer={issuer}
     * https://github.com/google/google-authenticator/wiki/Key-Uri-Format
     * 参数须要进行 url 编码 + 号须要替换成 %20
     * @param secret 密钥 应用 createSecretKey 办法生成
     * @param account 用户账户 如: example@domain.com
     * @param issuer 服务名称 如: Google,GitHub
     * @throws UnsupportedEncodingException
     */
    @SneakyThrows
    public static String createKeyUri(String secret, String account, String issuer) throws UnsupportedEncodingException {String qrCodeStr = "otpauth://totp/${issuer}:${account}?secret=${secret}&issuer=${issuer}";
        ImmutableMap.Builder<String, String> mapBuilder = ImmutableMap.builder();
        mapBuilder.put("account", URLEncoder.encode(account, "UTF-8").replace("+", "%20"));
        mapBuilder.put("secret", URLEncoder.encode(secret, "UTF-8").replace("+", "%20"));
        mapBuilder.put("issuer", URLEncoder.encode(issuer, "UTF-8").replace("+", "%20"));
        return StringSubstitutor.replace(qrCodeStr, mapBuilder.build());
    }

    /**
     * 校验办法
     *
     * @param secretKey 密钥
     * @param totpCode TOTP 一次性明码
     * @return 验证后果
     */
    public static boolean verification(String secretKey, String totpCode) {long time = System.currentTimeMillis() / 1000 / 30;
        // 优先计算以后工夫, 而后再计算偏移量, 因为大部分状况下客户端与服务的工夫统一
        if (totpCode.equals(generateTOTP(secretKey, time))) {return true;}
        for (int i = -TIME_OFFSET; i <= TIME_OFFSET; i++) {
            // i == 0 的状况曾经算过
            if (i != 0) {if (totpCode.equals(generateTOTP(secretKey, time + i))) {return true;}
            }
        }
        return false;
    }

}

QRCodeUtils 提供了生成二维码的性能,用于用户应用手机 APP Google Authenticator 扫描二维码进行绑定

public class QRCodeUtils {/** 二维码宽度(默认) */
    private static final int WIDTH = 300;
    /** 二维码高度(默认) */
    private static final int HEIGHT = 300;
    /** 二维码文件格式 */
    private static final String FILE_FORMAT = "png";
    /** 二维码参数 */
    private static final Map<EncodeHintType, Object> HINTS = new HashMap<EncodeHintType, Object>();

    static {
        // 字符编码
        HINTS.put(EncodeHintType.CHARACTER_SET, "utf-8");
        // 容错等级 H 为最高
        HINTS.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
        // 边距
        HINTS.put(EncodeHintType.MARGIN, 2);
    }

    /**
     * 返回一个 BufferedImage 对象
     *
     * @param content 二维码内容
     */
    public static BufferedImage toBufferedImage(String content) throws WriterException {BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, WIDTH, HEIGHT, HINTS);
        return MatrixToImageWriter.toBufferedImage(bitMatrix);
    }

    /**
     * 返回一个 BufferedImage 对象
     *
     * @param content 二维码内容
     * @param width 宽
     * @param height 高
     */
    public static BufferedImage toBufferedImage(String content, int width, int height)
            throws WriterException {BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, HINTS);
        return MatrixToImageWriter.toBufferedImage(bitMatrix);
    }

    /**
     * 将二维码图片输入到一个流中
     *
     * @param content 二维码内容
     * @param stream 输入流
     */
    public static void writeToStream(String content, OutputStream stream) throws WriterException, IOException {BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, WIDTH, HEIGHT, HINTS);
        MatrixToImageWriter.writeToStream(bitMatrix, FILE_FORMAT, stream);
    }

    /**
     * 将二维码图片输入到一个流中
     *
     * @param content 二维码内容
     * @param stream 输入流
     * @param width 宽
     * @param height 高
     */
    public static void writeToStream(String content, OutputStream stream, int width, int height)
            throws WriterException, IOException {BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, HINTS);
        MatrixToImageWriter.writeToStream(bitMatrix, FILE_FORMAT, stream);
    }

    /**
     * 生成二维码图片文件
     *
     * @param content 二维码内容
     * @param path 文件保留门路
     */
    public static void createQRCodeFile(String content, String path) throws WriterException, IOException {BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, WIDTH, HEIGHT, HINTS);
        MatrixToImageWriter.writeToPath(bitMatrix, FILE_FORMAT, new File(path).toPath());
    }

    /**
     * 生成二维码图片文件
     *
     * @param content 二维码内容
     * @param path 文件保留门路
     * @param width 宽
     * @param height 高
     */
    public static void createQRCodeFile(String content, String path, int width, int height)
            throws WriterException, IOException {BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, HINTS);
        MatrixToImageWriter.writeToPath(bitMatrix, FILE_FORMAT, new File(path).toPath());
    }
}

一次性验证码 TOPT 类

public class TOTP {private TOTP() { }

    /**
     * This method uses the JCE to provide the crypto algorithm.
     * HMAC computes a Hashed Message Authentication Code with the
     * crypto hash algorithm as a parameter.
     *
     * @param crypto: the crypto algorithm (HmacSHA1, HmacSHA256,
     * HmacSHA512)
     * @param keyBytes: the bytes to use for the HMAC key
     * @param text: the message or text to be authenticated
     */

    private static byte[] hmac_sha(String crypto, byte[] keyBytes,
                                   byte[] text) {
        try {
            Mac hmac;
            hmac = Mac.getInstance(crypto);
            SecretKeySpec macKey =
                    new SecretKeySpec(keyBytes, "RAW");
            hmac.init(macKey);
            return hmac.doFinal(text);
        } catch (GeneralSecurityException gse) {throw new UndeclaredThrowableException(gse);
        }
    }


    /**
     * This method converts a HEX string to Byte[]
     *
     * @param hex: the HEX string
     *
     * @return: a byte array
     */

    private static byte[] hexStr2Bytes(String hex) {
        // Adding one byte to get the right conversion
        // Values starting with "0" can be converted
        byte[] bArray = new BigInteger("10" + hex, 16).toByteArray();

        // Copy all the REAL bytes, not the "first"
        byte[] ret = new byte[bArray.length - 1];
        for (int i = 0; i < ret.length; i++) {ret[i] = bArray[i + 1];
        }
        return ret;
    }

    private static final int[] DIGITS_POWER
            // 0 1  2   3    4     5      6       7        8
            = {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000};


    /**
     * This method generates a TOTP value for the given
     * set of parameters.
     *
     * @param key: the shared secret, HEX encoded
     * @param time: a value that reflects a time
     * @param returnDigits: number of digits to return
     *
     * @return: a numeric String in base 10 that includes
     *
     */

    public static String generateTOTP(String key,
                                      String time,
                                      String returnDigits) {return generateTOTP(key, time, returnDigits, "HmacSHA1");
    }


    /**
     * This method generates a TOTP value for the given
     * set of parameters.
     *
     * @param key: the shared secret, HEX encoded
     * @param time: a value that reflects a time
     * @param returnDigits: number of digits to return
     *
     * @return: a numeric String in base 10 that includes
     *
     */

    public static String generateTOTP256(String key,
                                         String time,
                                         String returnDigits) {return generateTOTP(key, time, returnDigits, "HmacSHA256");
    }


    /**
     * This method generates a TOTP value for the given
     * set of parameters.
     *
     * @param key: the shared secret, HEX encoded
     * @param time: a value that reflects a time
     * @param returnDigits: number of digits to return
     *
     * @return: a numeric String in base 10 that includes
     *
     */

    public static String generateTOTP512(String key,
                                         String time,
                                         String returnDigits) {return generateTOTP(key, time, returnDigits, "HmacSHA512");
    }


    /**
     * This method generates a TOTP value for the given
     * set of parameters.
     *
     * @param key: the shared secret, HEX encoded
     * @param time: a value that reflects a time
     * @param returnDigits: number of digits to return
     * @param crypto: the crypto function to use
     *
     * @return: a numeric String in base 10 that includes
     *
     */

    public static String generateTOTP(String key,
                                      String time,
                                      String returnDigits,
                                      String crypto) {int codeDigits = Integer.decode(returnDigits).intValue();
        String result = null;

        // Using the counter
        // First 8 bytes are for the movingFactor
        // Compliant with base RFC 4226 (HOTP)
        while (time.length() < 16) {time = "0" + time;}

        // Get the HEX in a Byte[]
        byte[] msg = hexStr2Bytes(time);
        byte[] k = hexStr2Bytes(key);
        byte[] hash = hmac_sha(crypto, k, msg);

        // put selected bytes into result int
        int offset = hash[hash.length - 1] & 0xf;

        int binary =
                ((hash[offset] & 0x7f) << 24) |
                        ((hash[offset + 1] & 0xff) << 16) |
                        ((hash[offset + 2] & 0xff) << 8) |
                        (hash[offset + 3] & 0xff);

        int otp = binary % DIGITS_POWER[codeDigits];

        result = Integer.toString(otp);
        while (result.length() < codeDigits) {result = "0" + result;}
        return result;
    }
}

写在最初:如果须要残缺 Demo 的小伙伴能够留言分割

正文完
 0