SpringBoot项目集成2FA双因素认证功能

一、什么是 2FA(双因素身份验证)?

双因素身份验证(2FA)是一种安全系统,要求用户提供两种不同的身份验证方式才能访问某个系统或服务。国内普遍做短信验证码这种的用的比较少,不过在国外的网站中使用双因素身份验证的还是很多的。用户通过使用验证器扫描二维码,就能在app上获取登录的动态口令,进一步加强了账户的安全性。

二、实现步骤

1、添加pom.xml依赖

        <!--谷歌双因素验证-->
        <dependency>
            <groupId>com.google.zxing</groupId>
            <artifactId>core</artifactId>
            <version>3.4.0</version>
        </dependency>
        <dependency>
            <groupId>com.google.zxing</groupId>
            <artifactId>javase</artifactId>
            <version>3.4.0</version>
        </dependency>
        <dependency>
            <groupId>com.warrenstrange</groupId>
            <artifactId>googleauth</artifactId>
            <version>1.5.0</version>
        </dependency>

2、用户表修改

        1、添加字段:twoFaSecret  双因素认证密钥

3、需求功能

        用户首次登录时为其生成双因素验证二维码及密钥,用户通过相应的app扫描二维码后,输入对应的安全码并进行校验,若校验成功,则进行绑定,二次登录仅需输入安全码即可。重置会清空用户绑定的密钥,在下次登陆时会重新生成双因素验证二维码及密钥进行重新绑定。

4、Controller

    @ApiOperation("获取密钥")
    @GetMapping("/getSecret")
    public ApiResult<TwoFaAuthInfoVo> getSecret(Integer userId) throws IOException, WriterException {
        return ApiResult.success(twoFaService.getSecret(userId));
    }

    @ApiOperation("校验双因素认证验证码")
    @GetMapping("/check")
    public ApiResult<Boolean> check(Integer userId, Integer code, String secret) {
        return ApiResult.success(twoFaService.check(userId, code, secret));
    }

    @ApiOperation("重置密钥")
    @GetMapping("/resetSecret")
    public ApiResult<String> resetSecret(Integer userId) {
        int i = twoFaService.resetSecret(userId);
        return i > 0 ? ApiResult.success("重置成功") : ApiResult.error("重置失败");
    }

5、ServiceImpl实现

ITwoFaServiceImpl

    @Resource
    private UserMapper userMapper;

    @Resource
    private ICredentialRepository credentialRepository;

    /**
     * 重置双因素验证密钥
     *
     * @param userId 用户id
     */
    @Override
    public int resetSecret(Integer userId) {
        LambdaUpdateWrapper<User> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper.eq(User::getId, userId)
                .set(User::getTwoFaSecret, "");
        return userMapper.update(null, updateWrapper);
    }

    /**
     * 获取双因素验证密钥
     */
    @Override
    public TwoFaAuthInfoVo getSecret(Integer userId) throws WriterException, IOException {
        User user = userMapper.selectById(userId);
        TwoFaAuthInfoVo twoFaInfo = new TwoFaAuthInfoVo();
        if (StringUtils.isNotEmpty(user.getTwoFaSecret())) {
            twoFaInfo.setIsFirstLogin(false);
        }else {
            GoogleAuthenticator authenticator = getGoogleAuthenticator();
            GoogleAuthenticatorKey key = authenticator.createCredentials(user.getUsername());
            twoFaInfo = createTwoFaInfo(user, key.getKey());
            twoFaInfo.setIsFirstLogin(true);
        }
        return twoFaInfo;
    }

    /**
     * 验证码校验
     *
     * @param userId 用户id
     * @param code   验证码
     */
    @Override
    public Boolean check(Integer userId, Integer code, String secret) {
        User user = userMapper.selectById(userId);
        GoogleAuthenticator gAuth = getGoogleAuthenticator();
        //首次绑定验证
        if(StringUtils.isNotEmpty(secret)){
            user.setTwoFaSecret(secret);
            userMapper.updateById(user);
        }
        boolean result = gAuth.authorizeUser(user.getUsername(), code);
        //如果首次绑定验证失败
        if (!result && StringUtils.isNotEmpty(secret)){
            user.setTwoFaSecret("");
            userMapper.updateById(user);
        }
        return result;
    }

    /**
     * 创建双因素认证信息
     */
    private TwoFaAuthInfoVo createTwoFaInfo(User user, String secret) throws WriterException, IOException {
        // 使用内联配置创建GoogleAuthenticatorKey
        GoogleAuthenticatorKey credentials = new GoogleAuthenticatorKey.Builder(secret)
                .setConfig(createGoogleAuthConfig())
                .build();

        String otpAuthTotpURL = GoogleAuthenticatorQRGenerator.getOtpAuthTotpURL(
                “测试项目”,
                user.getUsername(),
                credentials
        );

        TwoFaAuthInfoVo infoVo = new TwoFaAuthInfoVo();
        infoVo.setQrCodeBase64(generateQR(otpAuthTotpURL));
        infoVo.setUserName(user.getUsername());
        infoVo.setSecret(secret);
        return infoVo;
    }

    /**
     * 生成二维码
     */
    private String generateQR(String otpAuthTotpURL) throws WriterException, IOException {
        QRCodeWriter qrWriter = new QRCodeWriter();
        BitMatrix bitMatrix = qrWriter.encode(otpAuthTotpURL, BarcodeFormat.QR_CODE, 200, 200);
        BufferedImage bufferedImage = MatrixToImageWriter.toBufferedImage(bitMatrix);
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        ImageIO.write(bufferedImage, "png", outputStream);
        return "data:image/png;base64," + Base64.getEncoder().encodeToString(outputStream.toByteArray());
    }

    /**
     * 创建GoogleAuthenticator配置(复用此配置)
     */
    private GoogleAuthenticatorConfig createGoogleAuthConfig() {
        return new GoogleAuthenticatorConfig.GoogleAuthenticatorConfigBuilder()
                .setTimeStepSizeInMillis(30 * 1000L)   //安全码有效时间,单位毫秒
                .setCodeDigits(6)    //安全码位数
                .setWindowSize(1)    //允许的误差时间窗口范围,通常以时间步长(time step)为单位
                .build();
    }

    /**
     * 获取配置好的GoogleAuthenticator实例
     */
    private GoogleAuthenticator getGoogleAuthenticator() {
        GoogleAuthenticator authenticator = new GoogleAuthenticator(createGoogleAuthConfig());
        authenticator.setCredentialRepository(credentialRepository);
        return authenticator;
    }

ICredentialRepositoryImpl:

public class ICredentialRepositoryImpl implements ICredentialRepository {

    @Resource
    private UserMapper userMapper;

    @Override
    public String getSecretKey(String userName) {
        log.info("获取用户密钥:{}", userName);
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getUsername, userName).last("limit 1");
        User user = userMapper.selectOne(queryWrapper);
        return user.getTwoFaSecret();
    }

    @Override
    public void saveUserCredentials(String userName,
                                    String secretKey,
                                    int validationCode,
                                    List<Integer> scratchCodes) {
    }
}

针对ITwoFaServiceImpl类中的createGoogleAuthConfig()方法可自定义相关配置,但是针对于市面上不同的2FA应用存在兼容问题, 如Microsoft的Authenticator仅支持6位验证码,Authing令牌该应用支持8位验证码等,建议使用默认配置 

6、TwoFaAuthInfoVo

@Data
@Accessors(chain = true)
@ApiModel(value = "双因素认证对象", description = "双因素认证对象")
public class TwoFaAuthInfoVo implements Serializable {

    @ApiModelProperty(value = "二维码base64")
    private String qrCodeBase64;

    @ApiModelProperty(value = "用户名")
    private String userName;

    @ApiModelProperty(value = "密钥")
    private String secret;

    @ApiModelProperty(value = "是否首次登录")
    private Boolean isFirstLogin;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值