转载

手把手教你集成Google Authenticator实现多因素认证(MFA)

近年来层出不穷的安全漏洞表明,单单只通过密码不能保障真正的安全,为了进一步增强身份认证的过程本文在校验用户名和密码之后再增加一层基于Goole Authenticator的认证来提高系统的安全性。

操作方式:

  • 1、手机应用商店下载Google Authenticator
  • 2、调用LoginService的generateGoogleAuthQRCode方法生成二维码 13
  • 3、使用手机打开Google Authenticator扫描该二维码进行绑定,绑定后会每隔30秒生成一次6位动态验证码 65
  • 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的小伙伴可以留言联系
正文到此结束
Loading...