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;
}
}