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 {@Beanpublic 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";@Resourceprivate 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 convertedbyte[] 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 intint 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;}
}