概念:
二次验证这个功能又叫双因素认证_百度百科或者叫双因子认证
双因素认证是一种采用时间同步技术的系统,采用了基于时间、事件和密钥三变量而产生的一次性密码来代替传统的静态密码。每个动态密码卡都有一个唯一的密钥,该密钥同时存放在服务器端,每次认证时动态密码卡与服务器分别根据同样的密钥,同样的随机参数(时间、事件)和同样的算法计算了认证的动态密码,从而确保密码的一致性,从而实现了用户的认证。因每次认证时的随机参数不同,所以每次产生的动态密码也不同。由于每次计算时参数的随机性保证了每次密码的不可预测性,从而在最基本的密码认证这一环节保证了系统的安全性。解决因口令欺诈而导致的重大损失,防止恶意入侵者或人为破坏,解决由口令泄密导致的入侵问题。
我最近在项目中遇到需要在登录的时候加入双因素验证系统的功能需求, 整体步骤感觉还是有必要写下来,方便做类似功能的同学能够借鉴。
google-Authenticator在国内可能并不适用,于是需要寻找一个能在国内稳定使用的二次验证系统
微软的Microsoft-Authenticator二次验证系统可以说是比较合适的
官方的二次验证的流程如下:
工具类
这个工具类就放在你的项目中,就代表你的这个功能做了有一半了
getQrCodeText方法是生成二维码的内容
getQrCode方法是生成二维码
@Component
public class TwoFactorAuthUtil {
@Autowired
private ParameterStubV1 parameterStubV1;
/**
* 生成二维码内容 网站地址(可不写)
* @return
*/
public static String getQrCodeText(String secretKey, String account, String issuer) {
String normalizedBase32Key = secretKey.replace(" ", "").toUpperCase();
try {
String Url = null;
Url = "otpauth://totp/"
+ URLEncoder.encode((!StringUtils.isEmpty(issuer) ? (issuer + ":") : "") + account, "UTF-8").replace("+", "%20")
+ "?secret=" + URLEncoder.encode(normalizedBase32Key, "UTF-8").replace("+", "%20")
+ (!StringUtils.isEmpty(issuer) ? ("&issuer=" + URLEncoder.encode(issuer, "UTF-8").replace("+", "%20")) : "");
//log.info(Url);
return Url;
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException(e);
}
}
/**
* description:
* @author 获取二维码
*/
public String getQrCode(String loginName, String newSecretKey) {
String base64Image = null;
try {
// 生成二维码内容
String qrCodeText = getQrCodeText(newSecretKey, loginName, "");
int width = 300; // 图片宽度
int height = 300; // 图片高度
try {
// 将URL转换为BitMatrix
QRCodeWriter qrCodeWriter = new QRCodeWriter();
BitMatrix bitMatrix = qrCodeWriter.encode(qrCodeText, BarcodeFormat.QR_CODE, width, height);
// 将BitMatrix转换为BufferedImage
BufferedImage bufferedImage = MatrixToImageWriter.toBufferedImage(bitMatrix);
// 保存二维码图片到本地文件
//File file = new File("D:\\图片\\qrcode.png");
//ImageIO.write(bufferedImage, format, file);
//log.info("QR Code image generated successfully!");
// 生成二维码图像
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
MatrixToImageWriter.writeToStream(bitMatrix, "png", outputStream);
// 获取图像的字节数组,并使用 Base64 编码转换成字符串
byte[] imageData = outputStream.toByteArray();
base64Image = AppConfigUtil.getStringValue(TwoFactorAuthConstant.MICROSOFT_AUTH_BASE64_IMAGE) + java.util.Base64.getEncoder().encodeToString(imageData);
return base64Image;
} catch (WriterException e) {
throw new BusinessException(I18nMsg.tr("生成二维码图像失败"));
} catch (Exception e) {
throw new BusinessException("Unexpected error:" + e.getMessage());
}
} catch (BusinessException e) {
throw new BusinessException(I18nMsg.tr("生成二维码发生异常:{0}", e.getMessage()));
}
}
}
二次验证的接口:
这个是策略类的接口,除了使用微软的验证器,你还可以实现这个接口来使用其他品牌的验证器
public interface TwoFactorAuthProvider {
//生成密钥
String getSecretKey();
//校验密钥
boolean checkCode(String secret, long code, long time);
}
实现类代码如下:
实现类主要是以下几个重要内容
时间前后偏移量
用于防止客户端时间不精确导致生成的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 int WINDOW_SIZE = 1;
加密方式,HmacSHA1、HmacSHA256、HmacSHA512
private static final String CRYPTO = "HmacSHA1";
getSecretKey方法用来生成用户密钥,生成的密钥记得做持久化
checkCode方法用来校验验证码是否正确
@Service
public class MicrosoftAuth implements TwoFactorAuthProvider {
// 私有构造方法
private MicrosoftAuth() {
}
// 在成员位置创建该类的对象
private static MicrosoftAuth microsoftAuth;
// 对外提供静态方法获取该对象
public static synchronized MicrosoftAuth getMicrosoftAuth() {
if (microsoftAuth == null) {
microsoftAuth = new MicrosoftAuth();
}
return microsoftAuth;
}
/**
* 时间前后偏移量
* 用于防止客户端时间不精确导致生成的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 int WINDOW_SIZE = 1;
/**
* 加密方式,HmacSHA1、HmacSHA256、HmacSHA512
*/
private static final String CRYPTO = "HmacSHA1";
/**
* description: 生成密钥,每个用户独享一份密钥
*
* @return java.lang.String
* @author litong
* @date 2023/4/27
*/
@Override
public String getSecretKey() {
String secretKey = null;
try {
SecureRandom random = new SecureRandom();
byte[] bytes = new byte[20];
random.nextBytes(bytes);
Base32 base32 = new Base32();
secretKey = base32.encodeToString(bytes).toUpperCase();
} catch (Exception e) {
throw new BusinessException(I18nMsg.tr("生成密钥发生异常:{0}", e.getMessage()));
}
// make the secret key more human-readable by lower-casing and
// inserting spaces between each group of 4 characters
return secretKey;
}
/**
* 检验 code 是否正确
*
* @param secret 密钥
* @param code code
* @param time 时间戳
* @return
*/
@Override
public boolean checkCode(String secret, long code, long time) {
boolean flag = false;
try {
Base32 codec = new Base32();
byte[] decodedKey = codec.decode(secret);
// convert unix msec time into a 30 second "window"
// this is per the TOTP spec (see the RFC for details)
long t = (time / 1000L) / 30L;
// Window is used to check codes generated in the near past.
// You can use this value to tune how far you're willing to go.
long hash;
for (int i = -WINDOW_SIZE; i <= WINDOW_SIZE; ++i) {
try {
hash = verifyCode(decodedKey, t + i);
} catch (Exception e) {
// Yes, this is bad form - but
// the exceptions thrown would be rare and a static
// configuration problem
// e.printStackTrace();
throw new RuntimeException(e.getMessage());
}
if (hash == code) {
flag = true;
return flag;
}
}
} catch (RuntimeException e) {
throw new BusinessException(I18nMsg.tr("校验密钥发生异常:{0}", e.getMessage()));
}
return flag;
}
/**
* 根据时间偏移量计算
*
* @param key
* @param t
* @return
* @throws NoSuchAlgorithmException
* @throws InvalidKeyException
*/
private static long verifyCode(byte[] key, long t) throws NoSuchAlgorithmException, InvalidKeyException {
try {
byte[] data = new byte[8];
long value = t;
for (int i = 8; i-- > 0; value >>>= 8) {
data[i] = (byte) value;
}
SecretKeySpec signKey = new SecretKeySpec(key, CRYPTO);
Mac mac = Mac.getInstance(CRYPTO);
mac.init(signKey);
byte[] hash = mac.doFinal(data);
int offset = hash[20 - 1] & 0xF;
// We're using a long because Java hasn't got unsigned int.
long truncatedHash = 0;
for (int i = 0; i < 4; ++i) {
truncatedHash <<= 8;
// We are dealing with signed bytes:
// we just keep the first byte.
truncatedHash |= (hash[offset + i] & 0xFF);
}
truncatedHash &= 0x7FFFFFFF;
truncatedHash %= 1000000;
return truncatedHash;
} catch (NoSuchAlgorithmException | InvalidKeyException | IllegalStateException e) {
throw new BusinessException(I18nMsg.tr("生成系统验证密钥出现异常{0}",e.getMessage()));
}
}
}
总结
简单应用到系统中的步骤如下:
1.定义工具类和实现类
2.编写获取用户名密钥的接口,生成的密钥做持久化
3.使用工具类将密钥和用户名生成二维码,返回给前端,显示大概如下:
4.使用二次验证APP扫描二维码进行绑定,软件会自动生成验证码
5.前端传递对应的验证码到后端,后端使用工具类中的方法校验,返回值为boolean类型
这样系统就将微软二次验证系统接入到你的系统里面啦