Spring Boot接入多因子认证(2FA|MFA)

客户端

注意

  • 服务端时间必须与客户端一致
  • 接入自己的用户系统时,可以设计一个 user - secret 一一映射的表用来绑定身份验证。本案例偷懒了,直接放内存里的
  • 二维码的展示放在了 JSON 中,使用了国内可以访问的一个 API,可能会失效

快速开始

  • 用户下载、安装好客户端应用后,尝试发出此请求
curl -XGET localhost:8080/bind?username=test
  • 将返回 JSON 中的二维码地址通过浏览器打开,或者使用 wget 等客户端下载到本地

  • 使用客户端导入此二维码,或者填写密文导入。导入成功后,应用会出现相应的 6 位数验证码

  • 将验证码作为以下 HTTP 请求的 codeInput 字段内容

curl -XGET localhost:8080/check?username=test&codeInput={codeInput}
  • 返回的结果为 true 则验证成功

代码实现

工具类

MultiFactorAuthenticator.java

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base32;
import org.apache.commons.codec.binary.Base64;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

@Slf4j
public class MultiFactorAuthenticator {
    // this is the issuer, you can change it to your company/project name.
    private static final String ISSUER = "Hello";
    // this is a http api (GET request) to generate the QR code image to show in the website/app.
    private static final String IMAGE_QR_CODE_API = "https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=";
    // you can change it to any string.
    private static final String SEED = "thisisgoogleauthenticator";
    // taken from Google pam docs - we probably don't need to mess with these
    private static final String RANDOM_NUMBER_ALGORITHM = "SHA1PRNG";
    private static final int SECRET_SIZE = 10;
    // suggest: the smaller the value, the safer it is. (from 1 to 17)
    private static final int WINDOW_SIZE = 1;

    public static String generateSecretKey() {
        try {
            SecureRandom sr = SecureRandom.getInstance(RANDOM_NUMBER_ALGORITHM);
            sr.setSeed(Base64.decodeBase64(SEED));
            byte[] buffer = sr.generateSeed(SECRET_SIZE);
            Base32 codec = new Base32();
            byte[] bEncodedKey = codec.encode(buffer);
            return new String(bEncodedKey);
        } catch (NoSuchAlgorithmException e) {
            log.error("generate secret exception:{[]}", e);
        }
        return null;
    }

    public static String getQRBarcodeURL(String user, String secret) {
        String format = IMAGE_QR_CODE_API + "otpauth://totp/%s?secret=%s%%26issuer=%s";
        String imageUrl = String.format(format, user, secret, ISSUER);
        log.info(imageUrl);
        return imageUrl;
    }

    public static boolean checkCode(String secret, long code, long time) {
        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.
        for (int i = -WINDOW_SIZE; i <= WINDOW_SIZE; ++i) {
            long hash;
            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());
                //return false;
            }
            if (hash == code) {
                return true;
            }
        }
        // The validation code is invalid.
        return false;
    }

    private static int verifyCode(byte[] key, long t) throws NoSuchAlgorithmException, InvalidKeyException {
        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, "HmacSHA1");
        Mac mac = Mac.getInstance("HmacSHA1");
        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 (int) truncatedHash;
    }

    private MultiFactorAuthenticator() {
    }
}

接入自己的用户体系

TestController.java

import com.auth.www.util.MultiFactorAuthenticator;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Authentication Bind / Check Interface
 */
@Slf4j
@RestController
public class TestController {

    /**
     * We can use a Map to simulate the database.
     */
    protected Map<String, String> userSecret = new ConcurrentHashMap<>();

    /**
     * Bind user to the secret.
     *
     * @param username username
     * @return qr code
     */
    @GetMapping("/bind")
    public Map<String, String> bind(@RequestParam String username) {
        HashMap<String, String> json = new HashMap<>(4);
        if (userSecret.containsKey(username)) {
            String secret = userSecret.get(username);
            String qrCodeLink = MultiFactorAuthenticator.getQRBarcodeURL(username, secret);
            json.put("qrCodeLink", qrCodeLink);
            json.put("user", username);
            json.put("secret", secret);
            log.info(userSecret.toString());
            return json;
        }
        String secret = MultiFactorAuthenticator.generateSecretKey();
        String qrCodeLink = MultiFactorAuthenticator.getQRBarcodeURL(username, secret);
        json.put("qrCodeLink", qrCodeLink);
        json.put("user", username);
        json.put("secret", secret);
        userSecret.put(username, secret);
        log.info(userSecret.toString());
        return json;
    }

    /**
     * check user's code
     *
     * @param username  user name
     * @param codeInput input from user
     * @return right or not
     */
    @GetMapping("/check")
    public Map<String, Object> check(@RequestParam String username, @RequestParam String codeInput) {
        HashMap<String, Object> json = new HashMap<>(2);
        String secret = userSecret.get(username);
        if (null != codeInput && codeInput.length() == 6 && null != secret) {
            long code = Long.parseLong(codeInput);
            boolean result = MultiFactorAuthenticator.checkCode(secret, code, System.currentTimeMillis());
            json.put("pass", result);
        } else {
            json.put("pass", false);
        }
        log.info(userSecret.toString());
        return json;
    }
}

单元测试

MultiFactorAuthenticatorApplicationTests.java

import com.auth.www.util.MultiFactorAuthenticator;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class MultiFactorAuthenticatorApplicationTests {

    @Test
    public void genSecretTest() {
        String secret = MultiFactorAuthenticator.generateSecretKey();
        String qrCode = MultiFactorAuthenticator.getQRBarcodeURL("username", secret);
        System.out.println("secret = " + secret);
        System.out.println("qrCode = " + qrCode);
    }

    @Test
    public void verifyTest() {
        String secret = "ABCDEFGHIJKLMN";
        String randomCode = "012345";
        long code = Long.parseLong(randomCode);
        boolean result = MultiFactorAuthenticator.checkCode(secret, code, System.currentTimeMillis());
        System.out.println(result);
    }
}

版权声明:
作者:Joe.Ye
链接:https://www.appblog.cn/index.php/2023/03/26/spring-boot-access-multifactor-authentication-2fa-mfa/
来源:APP全栈技术分享
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
打赏
海报
Spring Boot接入多因子认证(2FA|MFA)
客户端 Google Authenticator Authy Step Two 注意 服务端时间必须与客户端一致 接入自己的用户系统时,可以设计一个 user - secret 一一映射的表用来绑……
<<上一篇
下一篇>>
文章目录
关闭
目 录