目录
前言
双因子认证(2FA),有时又被称作两步验证或者双因素验证,是一种主流安全验证过程。在这一验证过程中,需要用户提供两种不同的认证因素来证明自己的身份(通俗一点说即身份验证涉及两个阶段,通常是除了常规账密登录以外的另一种验证方式,比如短信验证,TOTP动态口令验证),从而更好地保护用户证书和用户可访问的资源。
从正常业务来说,未开启双因子认证时,用户可通过账密、微信扫码、短信验证码等单认证方式进行登录,当开启双因子认证后,用户需要通过账密登录+短信验证码/TOTP动态口令才能完成登陆,具体选择通过后台运维配置实现。短信验证码方式比较简单(在国内使用短信验证码方式实现2FA其实比较常见,但似乎不被某些专家认可),直接忽略,本文介绍下TOTP模式
TOTP
介绍
全程是基于时间的一次性口令(Time-Based One-Time Password),它是公认的可靠解决方案,已经写入国际标准RFC6238。目前支持TOTP的应用app:
- 腾讯身份验证器
- Google Authenticator
- Microsoft Authenticator
个人推荐谷歌和微软的,腾讯的在某几次测试下存在问题
Java实现
首先引入maven依赖
<dependency>
<groupId>org.jboss.aerogear</groupId>
<artifactId>aerogear-otp-java</artifactId>
<version>1.0.0</version>
</dependency>
totp工具
public class TotpUtils {
public static String generateSecretKey() {
return Base32.random();
}
public static String getTotpUri(String secretKey, String username) {
return "otpauth://totp/YourAppName:" + username + "?secret=" + secretKey + "&issuer=YourAppName";
}
public static boolean verifyCode(String code, String secretKey) {
Totp totp = new Totp(secretKey);
return totp.verify(code);
}
}
上面工具类的 getTotpUri 方法用于生成二维码,以供用户使用身份验证App进行扫码绑定,二维码工具类如下,可参考
<!-- zxing生成二维码 -->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.3.3</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.3.3</version>
</dependency>
public class QRCodeUtil {
//CODE_WIDTH:二维码宽度,单位像素
private static final int CODE_WIDTH = 400;
//CODE_HEIGHT:二维码高度,单位像素
private static final int CODE_HEIGHT = 400;
//FRONT_COLOR:二维码前景色,0x000000 表示黑色
private static final int FRONT_COLOR = 0x000000;
//BACKGROUND_COLOR:二维码背景色,0xFFFFFF 表示白色
private static final int BACKGROUND_COLOR = 0xFFFFFF;
public static void createCodeToFile(String content, File codeImgFileSaveDir, String fileName) {
try {
if (StringUtils.isEmpty(content) || StringUtils.isEmpty(fileName)) {
return;
}
content = content.trim();
if (codeImgFileSaveDir==null || codeImgFileSaveDir.isFile()) {
//二维码图片存在目录为空,默认放在桌面...
codeImgFileSaveDir = FileSystemView.getFileSystemView().getHomeDirectory();
}
if (!codeImgFileSaveDir.exists()) {
//二维码图片存在目录不存在,开始创建...
codeImgFileSaveDir.mkdirs();
}
//核心代码-生成二维码
BufferedImage bufferedImage = getBufferedImage(content);
File codeImgFile = new File(codeImgFileSaveDir, fileName);
ImageIO.write(bufferedImage, "png", codeImgFile);
log.info("二维码图片生成成功:" + codeImgFile.getPath());
} catch (Exception e) {
log.error("二维码图片生成异常 {}", e.getMessage());
}
}
/**
* 生成二维码并输出到输出流
* @param content :二维码内容
* @param outputStream :输出流
*/
public static void createCodeToOutputStream(String content, OutputStream outputStream) {
try {
if (StringUtils.isEmpty(content)) {
return;
}
content = content.trim();
//核心代码-生成二维码
BufferedImage bufferedImage = getBufferedImage(content);
//区别就是这一句,输出到输出流中,如果第三个参数是 File,则输出到文件中
ImageIO.write(bufferedImage, "png", outputStream);
log.info("二维码图片生成到输出流成功...");
} catch (Exception e) {
log.error("二维码图片生成到输出流异常 {}", e.getMessage());
}
}
//核心代码-生成二维码
public static BufferedImage getBufferedImage(String content) throws WriterException {
//com.google.zxing.EncodeHintType:编码提示类型,枚举类型
Map<EncodeHintType, Object> hints = new HashMap();
//EncodeHintType.CHARACTER_SET:设置字符编码类型
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
//EncodeHintType.ERROR_CORRECTION:设置误差校正
//ErrorCorrectionLevel:误差校正等级,L = ~7% correction、M = ~15% correction、Q = ~25% correction、H = ~30% correction
//不设置时,默认为 L 等级,等级不一样,生成的图案不同,但扫描的结果是一样的
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
//EncodeHintType.MARGIN:设置二维码边距,单位像素,值越小,二维码距离四周越近
hints.put(EncodeHintType.MARGIN, 1);
MultiFormatWriter multiFormatWriter = new MultiFormatWriter();
BitMatrix bitMatrix = multiFormatWriter.encode(content, BarcodeFormat.QR_CODE, CODE_WIDTH, CODE_HEIGHT, hints);
BufferedImage bufferedImage = new BufferedImage(CODE_WIDTH, CODE_HEIGHT, BufferedImage.TYPE_INT_BGR);
for (int x = 0; x < CODE_WIDTH; x++) {
for (int y = 0; y < CODE_HEIGHT; y++) {
bufferedImage.setRGB(x, y, bitMatrix.get(x, y) ? FRONT_COLOR : BACKGROUND_COLOR);
}
}
return bufferedImage;
}
}
用户扫描系统生成的二维码后效果图如下:
具体使用时就更简单了,比如用户注册时生成密钥并入库,登陆认证时获取到对应用户的密钥然后调用上面的工具类校验即可。
小插曲
- totp的授权绑定url如果包含中文,扫码会失败
- totp的授权绑定url中如果携带period参数,想额外控制身份验证应用里码的有效时间,似乎不生效,App里永远都是显示的默认30s倒计时,代码里同样也将时间步长改为60,总归得不到我期望的结果,不太懂,本想着尝试下改成60s,失败告终