SpringBoot-集成TOTP

TOTP验证码提供了一种高效且安全的身份验证方法。它不仅减少了依赖短信或其他通信方式带来的成本和延时,还通过不断变换的密码增加了破解的难度。未来,随着技术的进步和对安全性要求的提高,TOTP及其衍生技术将继续发展并被更广泛地应用。TOTP验证码是基于时间的一次性密码算法(Time-based One-Time Password algorithm)。其核心原理是使用预共享密钥和当前时间戳生成一次性的验证码。
TOTP验证码的概念及相关分析:

定义与用途:TOTP,即基于时间的一次性密码算法,是一种利用时间同步和双方预共享的密钥来生成一次性密码的方法。这种方法主要用于双因素认证(2FA),提高账户安全性。

工作机制:在TOTP中,服务器和客户端都会预先共享一个密钥。当需要验证用户身份时,客户端会基于当前时间和该预共享密钥生成一个OTP(一次性密码)。只有知道正确的密钥和准确时间的用户才能生成正确的OTP,从而通过验证。

安全性增强:由于每次认证都使用新的密码且仅在短时间内有效,这使得TOTP比传统的静态密码更为安全。即使攻击者截获了一次密码,也因其很快就过期而无法再次使用。

性能优化:与传统的短信发送验证码相比,TOTP不需要通信费用,且响应速度更快,因为它仅依赖于时间同步而非外部通信。

广泛应用:多数现代认证系统如Google Authenticator和其他多种第三方认证应用都支持TOTP,使其成为事实上的标准之一。

TOTP验证码的原理及相关分析:

密钥预共享:服务端生成并通过安全的渠道分发一个唯一的密钥给客户端。这个密钥是后续所有操作的基础。

时间戳的使用:客户端根据当前时间和预共享的密钥计算一次性密码。这个过程通常每30秒进行一次,确保密码的新鲜性和安全性。

HMAC-SHA1算法:使用预共享密钥和当前的时间计数作为输入,通过HMAC-SHA1算法生成一串固定长度的输出值。此输出经过特定处理后被转换为较短的数位,形成最终的验证码。

服务器验证:当用户提交OTP时,服务器也会使用相同的方法和密钥计算当时的OTP应是什么,并与用户提供的值进行比较,以此判断用户的验证请求是否有效。

容错机制:考虑到客户端和服务端的时钟可能不完全同步,TOTP算法允许有一定的时间容错,通常为前后几秒的时间窗口内,这保证了合法用户的正常体验。

以下是关于TOTP验证码的普及与应用以及注意事项:

普及与应用:随着移动设备的普及和互联网安全问题的增加,TOTP作为一种安全便捷的认证方式,正在被越来越多的场景所采用,包括企业级应用、金融服务、在线教育平台等。
注意事项:虽然TOTP提高了安全性,但仍应注意保护预共享密钥的安全,避免密钥泄露导致的潜在风险。同时,应定期更新密钥,防止长时间使用同一密钥增加的风险。

二维码是,下面label和issuer随意生成,secret是服务端根据每个用户生成

otpauth://totp/{label}?secret={secret}&issuer={issuer}

写一个starter

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>sf-framework</artifactId>
        <groupId>cn.nexteer.boot</groupId>
        <version>${revision}</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>sf-spring-boot-starter-otp</artifactId>
    <packaging>jar</packaging>
    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>cn.nexteer.boot</groupId>
            <artifactId>sf-common</artifactId>
        </dependency>

        <!-- RPC 远程调用相关 -->
        <dependency>
            <groupId>cn.nexteer.boot</groupId>
            <artifactId>sf-spring-boot-starter-rpc</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- 业务组件 -->
        <dependency>
            <groupId>cn.nexteer.boot</groupId>
            <artifactId>sf-module-system-api</artifactId> <!-- 需要使用它,进行 Token 的校验 -->
            <version>${revision}</version>
        </dependency>

        <!-- Spring 核心 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <!-- Web 相关 -->
        <dependency>
            <groupId>cn.nexteer.boot</groupId>
            <artifactId>sf-spring-boot-starter-web</artifactId>
            <scope>provided</scope>
        </dependency>

        <!-- Web 相关 -->
        <dependency>
            <groupId>cn.nexteer.boot</groupId>
            <artifactId>sf-spring-boot-starter-security</artifactId>
        </dependency>

        <!-- DB 相关 -->
        <dependency>
            <groupId>cn.nexteer.boot</groupId>
            <artifactId>sf-spring-boot-starter-mybatis</artifactId>
        </dependency>

        <dependency>
            <groupId>cn.nexteer.boot</groupId>
            <artifactId>sf-spring-boot-starter-redis</artifactId>
        </dependency>


        <!-- Test 测试相关 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- 工具类相关 -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
        </dependency>
    </dependencies>
</project>

配置类

@AutoConfiguration
public class SfOtpAutoConfiguration {

    @Bean
    public OtpAuthAspect otpAuthAspect()
    {
        return new OtpAuthAspect();
    }
}
@AutoConfiguration
@EnableFeignClients(clients = AdminUserApi.class) // 主要是引入相关的 API 服务
public class SfAdminUserRpcAutoConfiguration {
}

SPI文件

在这里插入图片描述

切面类

@Aspect
@RequiredArgsConstructor
@Slf4j
public class OtpAuthAspect {
    static final String OTP_CODE_HEADER = "X-OTP-CODE";
    static final String OPT_CODE_PARAM = "otpCode";
    @Resource
    private AdminUserApi adminUserApi;

    @Before("@annotation(otpAuth)")
    public void beforePointCut(JoinPoint joinPoint, OtpAuth otpAuth) throws Throwable {
        LoginUser loginUser = getLoginUser();
        HttpServletRequest request = getRequest();
        String otpCode = getOtpCodeByRequest(request);
        if (StrUtil.isBlank(otpCode)) {
            log.error("[around][用户({}) 请求({}) 时,未传递 OTP 验证码]", loginUser.getId(), request.getRequestURI());
            throw new ServiceException(GlobalErrorCodeConstants.OTP_ERROR.getCode(), otpAuth.message());
        }
        String secret = getKeyByLoginUserId(loginUser.getId());
        if (StrUtil.isBlank(secret)) {
            log.error("[around][用户({}) 请求({}) 时,未配置 OTP 密钥]", loginUser.getId(), request.getRequestURI());
            throw new ServiceException(GlobalErrorCodeConstants.OTP_ERROR.getCode(), otpAuth.message());
        }
        boolean result = TotpUtils.verify(secret,otpCode);
        if (!result) {
            log.error("[around][用户({}) 请求({}) 时,OTP 验证码错误]", loginUser.getId(), request.getRequestURI());
            throw new ServiceException(GlobalErrorCodeConstants.OTP_ERROR.getCode(), otpAuth.message());
        }
    }

    private String getKeyByLoginUserId(Long id) {
        CommonResult<AdminUserRespDTO> user = adminUserApi.getUser(id);
        AdminUserRespDTO checkedData = user.getCheckedData();
        return checkedData.getOtpSecret();
    }

    private String getOtpCodeByRequest(HttpServletRequest request) {
        String header = request.getHeader(OTP_CODE_HEADER);
        if (StrUtil.isNotBlank(header)) {
            return header;
        }

        String attribute = (String)request.getAttribute(OPT_CODE_PARAM);
        if (StrUtil.isNotBlank(attribute)) {
            return attribute;
        }
        String parameter = request.getParameter(OPT_CODE_PARAM);
        if (StrUtil.isNotBlank(parameter)) {
            return parameter;
        }
        return null;
    }
}

工具类

@Slf4j
public class TotpUtils {


    private static int WINDOW_SIZE = 1;

    private static long X = 30;

    private TotpUtils() {}
 
    /**
     * 该方法使用JCE提供加密算法。
     * HMAC使用加密哈希算法作为参数计算哈希消息认证码。
     * @param crypto: the crypto algorithm (HmacSHA1, HmacSHA256,
     *                             HmacSHA512)
     * @param keyBytes: 用于HMAC密钥的字节
     * @param text: 用于HMAC密钥的字节数
     */
    private static byte[] hmac_sha(String crypto, byte[] keyBytes,
                                   byte[] text){
        try {
            Mac hmac;
            hmac = Mac.getInstance(crypto);
            SecretKeySpec macKey =
                    new SecretKeySpec(keyBytes, "RAW");
            hmac.init(macKey);
            return hmac.doFinal(text);
        } catch (GeneralSecurityException gse) {
            throw new UndeclaredThrowableException(gse);
        }
    }
 
    /**
     * This method converts a HEX string to Byte[]
     * @param hex: the HEX string
     * @return: a byte array
     */
    private static byte[] hexStr2Bytes(String hex){
        // Adding one byte to get the right conversion Values starting with "0" can be converted
        byte[] bArray = new BigInteger("10" + hex,16).toByteArray();
 
        // Copy all the REAL bytes, not the "first"
        byte[] ret = new byte[bArray.length - 1];
        for (int i = 0; i < ret.length; i++)
            ret[i] = bArray[i+1];
        return ret;
    }
 
    private static final int[] DIGITS_POWER
            // 0 1  2   3    4     5      6       7        8
            = {1,10,100,1000,10000,100000,1000000,10000000,100000000 };
 
    /**
     * This method generates a TOTP value for the given
     * set of parameters.
     *
     * @param key: the shared secret, HEX encoded
     * @param time: a value that reflects a time
     * @param returnDigits: number of digits to return
     *
     * @return: a numeric String in base 10 that includes truncationDigits digits
     */
    public static String generateTOTP(String key,
                                      String time,
                                      String returnDigits){
        return generateTOTP(key, time, returnDigits, "HmacSHA1");
    }
 
    /**
     * This method generates a TOTP value for the given
     * set of parameters.
     *
     * @param key: the shared secret, HEX encoded
     * @param time: a value that reflects a time
     * @param returnDigits: number of digits to return
     *
     * @return: a numeric String in base 10 that includes truncationDigits digits
     */
    public static String generateTOTP256(String key,
                                         String time,
                                         String returnDigits){
        return generateTOTP(key, time, returnDigits, "HmacSHA256");
    }
 
    /**
     * This method generates a TOTP value for the given
     * set of parameters.
     *
     * @param key: the shared secret, HEX encoded
     * @param time: a value that reflects a time
     * @param returnDigits: number of digits to return
     *
     * @return: a numeric String in base 10 that includes truncationDigits digits
     */
    public static String generateTOTP512(String key,
                                         String time,
                                         String returnDigits){
        return generateTOTP(key, time, returnDigits, "HmacSHA512");
    }
 
    /**
     * This method generates a TOTP value for the given
     * set of parameters.
     *
     * @param key: the shared secret, HEX encoded
     * @param time: a value that reflects a time
     * @param returnDigits: number of digits to return
     * @param crypto: the crypto function to use
     *
     * @return: a numeric String in base 10 that includes truncationDigits digits
     */
    public static String generateTOTP(String key,
                                      String time,
                                      String returnDigits,
                                      String crypto){
        int codeDigits = Integer.decode(returnDigits).intValue();
        String result = null;
 
        // Using the counter
        // First 8 bytes are for the movingFactor
        // Compliant with base RFC 4226 (HOTP)
        while (time.length() < 16 )
            time = "0" + time;
 
        // Get the HEX in a Byte[]
        byte[] msg = hexStr2Bytes(time);
        byte[] k = hexStr2Bytes(key);
 
        byte[] hash = hmac_sha(crypto, k, msg);
 
        // put selected bytes into result int
        int offset = hash[hash.length - 1] & 0xf;
 
        int binary =
                ((hash[offset] & 0x7f) << 24) |
                        ((hash[offset + 1] & 0xff) << 16) |
                        ((hash[offset + 2] & 0xff) << 8) |
                        (hash[offset + 3] & 0xff);
 
        int otp = binary % DIGITS_POWER[codeDigits];
 
        result = Integer.toString(otp);
        while (result.length() < codeDigits) {
            result = "0" + result;
        }
        return result;
    }
 
    /**
     * 验证动态口令是否正确
     * @param secretBase32 密钥
     * @param code 待验证的动态口令
     * @return
     */
    public static boolean verify(String secretBase32, String code){
        String secretHex = HexUtil.encodeHexStr(Base32Codec.Base32Decoder.DECODER.decode(secretBase32));
        long t = System.currentTimeMillis() / 1000L / X;
        
        for (int i = -WINDOW_SIZE; i <= WINDOW_SIZE; ++i) {
            String steps = Long.toHexString(t).toUpperCase();
            while (steps.length() < 16) steps = "0" + steps;

            String totp = generateTOTP(secretHex, steps, "6",
                    "HmacSHA1");
            if (code.equals(totp)) {
                return true;
            }
        }
        return false;
    }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值