背景

什么是MFA?

MFA:多因素认证(Multi Factor Authentication)

多因素认证MFA(Multi Factor Authentication)是一种简单有效的安全认证机制,指要求用户提供两种或两种以上的认证因素,在登录和敏感操作等场景下,除了用户名密码的基本认证,云平台将根据安全风险等级进行二次或多次认证。

MFA的工作原理是什么?

用户身份绑定多种身份认证手段,由云平台发起通过进行身份认证,具体的认证原理根据认证手段不同有所差异。

MFA有哪些实际应用?

主要应用于:

  • 登录二次认证。为防止用户名密码泄露,在用户输入密码完成一次登录认证之后,云平台发起密码之外的另一种认证方式,对访问身份进行验证。

  • 敏感操作验证。在用户执行高风险操作,例如创建或删除云资源、变更元数据配置时,云平台发起二次认证,实时验证操作者身份。避免一些账号密码被盗、用户设备遗失等风险情况。

MFA有哪些类型?

常用的多因素认证手段可以根据验证的内容分为三种类型,以下列举各类型常用的MFA认证方式:

  1. 用户知道什么
  • 密码

  • 个人安全问题

  • 个人或企业实名信息:例如身份证号码、企业经营许可证编号、企业税号等。

  1. 用户有什么
  • 手机号/邮箱:发送验证码给用户。

  • 软件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/

“觉得文章还不错?微信扫一扫,赏作者一杯咖啡吧~”
分类
guest

0 评论
最旧
最新 最多投票
内联反馈
查看所有评论

相关文章

使用WireGuard在Ubuntu 24.04系统搭建VPN

WireGuard是什么? 维基百科是这样描...

Dockerfile 指令详解之COPY和ADD

COPY 复制文件 格式: COPY &lt...

Java设计模式总结

概念 软件设计模式(Software Des...

微服务架构中服务拆分粒度决策

在设计和实施微服务架构时,拆分粒度的决策非常...

Dubbo和Feign的区别

Feign与Dubbo性能对比及区别分析 随...