- 0
- 8 浏览
背景
什么是MFA?
MFA:多因素认证(Multi Factor Authentication)
多因素认证MFA(Multi Factor Authentication)是一种简单有效的安全认证机制,指要求用户提供两种或两种以上的认证因素,在登录和敏感操作等场景下,除了用户名密码的基本认证,云平台将根据安全风险等级进行二次或多次认证。
MFA的工作原理是什么?
用户身份绑定多种身份认证手段,由云平台发起通过进行身份认证,具体的认证原理根据认证手段不同有所差异。
MFA有哪些实际应用?
主要应用于:
-
登录二次认证。为防止用户名密码泄露,在用户输入密码完成一次登录认证之后,云平台发起密码之外的另一种认证方式,对访问身份进行验证。
-
敏感操作验证。在用户执行高风险操作,例如创建或删除云资源、变更元数据配置时,云平台发起二次认证,实时验证操作者身份。避免一些账号密码被盗、用户设备遗失等风险情况。
MFA有哪些类型?
常用的多因素认证手段可以根据验证的内容分为三种类型,以下列举各类型常用的MFA认证方式:
- 用户知道什么
-
密码
-
个人安全问题
-
个人或企业实名信息:例如身份证号码、企业经营许可证编号、企业税号等。
- 用户有什么
-
手机号/邮箱:发送验证码给用户。
-
软件TOTP(Time-based One-Time Password):虚拟MFA设备通过TOTP技术生成一次性密码进行验证,安装在手机、电脑等设备上。常见的如Google Authenticate、Microsoft Authenticate等,阿里云APP也提供了虚拟MFA功能。
-
硬件验证设备:如FIDO(Fast Identity Online)联盟推出的多因素认证协议U2F,目前已被业界广泛接受。用户只要将支持Web Authentication协议的硬件设备(称为U2F安全密钥)插入计算机的USB接口,即可在登录时通过触碰或按下设备上的按钮完成多因素认证。
本文主要讲解的是通过TOTP算法实现MFA。
正文
TOTP算法:
package site.lifd.core.crypto;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.lang.reflect.UndeclaredThrowableException;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
/**
* TOTP 算法
*
* @author lifengdi
* @createTime 2024/12/2 18:05
*/
public class TOTP {
private TOTP() {
}
/**
* 获取 TOTP
*
* @param key 密钥
* @return TOTP
*/
public static String getOTP(String key) {
return getOTP(getStep(), key);
}
/**
* 校验 TOTP
*
* @param key 密钥
* @param otp 待校验的 TOTP
* @return 校验结果
*/
public static boolean validate(String key, String otp) {
return validate(getStep(), key, otp);
}
/**
* 校验 TOTP
*
* @param step 步长
* @param key 密钥
* @param otp 待校验的 TOTP
* @return 校验结果
*/
private static boolean validate(long step, String key, String otp) {
return getOTP(step, key).equals(otp) || getOTP(step - 1L, key).equals(otp);
}
/**
* 获取当前 TOTP 步长
*
* @return 步长
*/
private static long getStep() {
return System.currentTimeMillis() / 30000L;
}
/**
* 获取 TOTP
*
* @param step 步长
* @param key 密钥
* @return TOTP
*/
private static String getOTP(long step, String key) {
String steps;
for(steps = Long.toHexString(step).toUpperCase(); steps.length() < 16; steps = "0" + steps) {
}
byte[] msg = hexStr2Bytes(steps);
byte[] k = hexStr2Bytes(key);
byte[] hash = hmac_sha1(k, msg);
int offset = hash[hash.length - 1] & 15;
int binary = (hash[offset] & 127) << 24 | (hash[offset + 1] & 255) << 16 | (hash[offset + 2] & 255) << 8 | hash[offset + 3] & 255;
int otp = binary % 1000000;
String result;
for(result = Integer.toString(otp); result.length() < 6; result = "0" + result) {
}
return result;
}
/**
* 将16进制字符串转换成字节数组
*
* @param hex 16进制字符串
* @return 字节数组
*/
private static byte[] hexStr2Bytes(String hex) {
byte[] bArray = (new BigInteger("10" + hex, 16)).toByteArray();
byte[] ret = new byte[bArray.length - 1];
System.arraycopy(bArray, 1, ret, 0, ret.length);
return ret;
}
/**
* HMAC-SHA1
*
* @param keyBytes 密钥
* @param text 待加密内容
* @return 加密结果
*/
private static byte[] hmac_sha1(byte[] keyBytes, byte[] text) {
try {
Mac hmac = Mac.getInstance("HmacSHA1");
SecretKeySpec macKey = new SecretKeySpec(keyBytes, "RAW");
hmac.init(macKey);
return hmac.doFinal(text);
} catch (GeneralSecurityException var4) {
GeneralSecurityException gse = var4;
throw new UndeclaredThrowableException(gse);
}
}
}
工具类:
package site.lifd.core.util;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import org.apache.commons.codec.binary.Base32;
import org.apache.commons.codec.binary.Hex;
import site.lifd.core.crypto.TOTP;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;
/**
* MFA工具类
*
* @author lifengdi
* @createTime 2024/12/2 19:43
*/
public class MFAUtil {
/**
* 生成32位的密钥
*
* @return 密钥
*/
public static String generateSecretKey() {
SecureRandom random = new SecureRandom();
byte[] bytes = new byte[20];
random.nextBytes(bytes);
Base32 base32 = new Base32();
return base32.encodeToString(bytes);
}
/**
* 获取TOTP验证码
*
* @param secretKey 密钥
* @return TOTP验证码
*/
public static String getTOTPCode(String secretKey) {
Base32 base32 = new Base32();
byte[] bytes = base32.decode(secretKey);
String hexKey = Hex.encodeHexString(bytes);
return TOTP.getOTP(hexKey);
}
/**
* 生成二维码内容字符串
*
* @param account 账户信息(展示在Google Authenticator App中的)
* @param secretKey 密钥
* @param title 标题 (展示在Google Authenticator App中的)
* @return 二维码内容字符串
*/
public static String generateScanQRString(String account, String secretKey, String title) {
return "otpauth://totp/"
+ URLEncoder.encode(title + ":" + account, StandardCharsets.UTF_8).replace("+", "%20")
+ "?secret=" + URLEncoder.encode(secretKey, StandardCharsets.UTF_8).replace("+", "%20")
+ "&issuer=" + URLEncoder.encode(title, StandardCharsets.UTF_8).replace("+", "%20");
}
/**
* 生成二维码图片
*
* @param account 账户信息(展示在Google Authenticator App中的)
* @param secretKey 密钥
* @param title 标题 (展示在Google Authenticator App中的)
* @return 二维码图片base64
* @throws WriterException 编码异常
* @throws IOException IO异常
*/
public static String generateQRCode(String account, String secretKey, String title) throws WriterException, IOException {
String barCodeData = generateScanQRString(account, secretKey, title);
return createQRCode(barCodeData, null, 300, 300);
}
public static void main(String[] args) throws IOException, WriterException {
String secretKey = generateSecretKey();
System.out.println("密钥:" + secretKey);
System.out.println("二维码:" + generateQRCode("admin", secretKey, "DineroTree"));
}
/**
* 生成二维码图片
*
* @param barCodeData 二维码内容
* @param outPath 二维码图片输出路径
* @param height 二维码图片高度
* @param width 二维码图片宽度
* @return 二维码图片base64
* @throws WriterException 编码异常
* @throws IOException IO异常
*/
public static String createQRCode(String barCodeData, String outPath, int height, int width)
throws WriterException, IOException {
BitMatrix matrix = new MultiFormatWriter().encode(barCodeData, BarcodeFormat.QR_CODE, width, height);
BufferedImage bufferedImage = MatrixToImageWriter.toBufferedImage(matrix);
ByteArrayOutputStream bof = new ByteArrayOutputStream();
ImageIO.write(bufferedImage, "png", bof);
String base64 = imageToBase64(bof.toByteArray());
if (StrUtil.isNotBlank(outPath)) {
try (FileOutputStream out = new FileOutputStream(outPath)) {
MatrixToImageWriter.writeToStream(matrix, "png", out);
}
}
return base64;
}
/**
* 图片转base64
*
* @param dataBytes 图片字节数组
* @return base64
*/
private static String imageToBase64(byte[] dataBytes) {
// 对字节数组Base64编码
if (dataBytes != null) {
return "data:image/jpeg;base64," + Base64.getEncoder().encodeToString(dataBytes);// 返回Base64编码过的字节数组字符串
}
return null;
}
}
实际应用DEMO:
@RestController
@RequestMapping("/mfa")
@Api(tags = {"多因子认证相关接口"})
public class MFAController {
@ApiOperation("口令验证")
@PostMapping("/verify")
public ApiResult<Boolean> verify(@RequestBody MFAModel requestParam) {
LoginUserModel loginUser = HttpContext.getLoginUser();
AdminUserPwd userPwd = userPwdService.getById(loginUser.getId());
String secretKey = userPwd.getSecretKey();
if (StringUtils.isBlank(secretKey)) {
throw new ServiceException("当前用户未绑定MFA");
}
String totpCode = MFAUtil.getTOTPCode(secretKey);
return ApiResult.of(totpCode.equals(model.getMfaCode()));
}
@ApiOperation("生成绑定二维码")
@GetMapping("/generateQRCodeByUserId")
public ApiResult<String> generateQRCodeByUserId(@RequestParam String userId) {
AdminUserPwd userPwd = userPwdService.getById(userId);
String secretKey = userPwd.getSecretKey();
return MFAUtil.generateQRCode("account", secretKey, "title");
}
}
代码地址:https://github.com/lifengdi/fdtools
本文链接:https://www.lifd.site/tech/java-dui-jie-mfa-duo-yin-zi-ren-zheng/