一、什么是 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;
}