核心设计思路
安全认证方案主要解决以下几个核心问题:
- 防重放攻击:通过时间戳和随机 Nonce 值限制请求的有效期
- 防请求篡改:通过 HMAC 签名验证请求参数的完整性
- 防时序攻击:使用安全的字符串比较方式验证令牌
- 密钥安全:通过密钥派生函数 (KDF) 增强原始密钥的安全性
核心代码实现与解析
数据结构定义
首先定义存储令牌信息的AuthToken类,包含时间戳、随机数和签名令牌三个核心字段:
public static class AuthToken {
private final long timestamp; // 时间戳(秒级)
private final String nonce; // 随机数
private final String token; // 签名令牌
public AuthToken(long timestamp, String nonce, String token) {
this.timestamp = timestamp;
this.nonce = nonce;
this.token = token;
}
// Getter方法...
}核心配置参数
定义认证方案的核心配置,可根据业务需求调整:
// 令牌有效期(5分钟)
private static final long TIME_WINDOW_SECONDS = TimeUnit.MINUTES.toSeconds(5);
// 加密算法
private static final String HMAC_ALGORITHM = "HmacSHA256";
// HKDF盐值和信息字段(增强密钥派生安全性)
private static final String HKDF_SALT = "HKDF_SALT";
private static final String HKDF_INFO = "HKDF_INFO";
// 随机数长度(16字节)
private static final int NONCE_LENGTH = 16;令牌生成逻辑
generateToken方法是整个认证方案的核心,负责生成包含时间戳、随机数和签名的完整令牌:
public static AuthToken generateToken(String clientId) {
// 1. 获取当前秒级时间戳
long timestamp = Instant.now().getEpochSecond();
// 2. 生成随机Nonce值(防止重放攻击)
String nonce = generateNonce();
// 3. 构建签名原文:时间戳_随机数_客户端ID
String message = String.join("_",
String.valueOf(timestamp),
nonce,
clientId != null ? clientId : ""
);
// 4. 密钥派生:通过双重HMAC增强密钥安全性
byte[] derivedKey = deriveKey((clientId != null ? clientId : "").getBytes(StandardCharsets.UTF_8));
// 5. 计算HMAC-SHA256签名
String token = hmacDigest(derivedKey, message);
return new AuthToken(timestamp, nonce, token);
}密钥派生函数
为了提升密钥安全性,我们实现了基于 HKDF 思想的密钥派生函数,通过双重 HMAC 处理原始密钥:
private static byte[] deriveKey(byte[] inputKey) {
try {
// 第一轮HMAC:使用盐值处理原始密钥
Mac hmac1 = Mac.getInstance(HMAC_ALGORITHM);
SecretKeySpec keySpec1 = new SecretKeySpec(HKDF_SALT.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM);
hmac1.init(keySpec1);
byte[] tempKey = hmac1.doFinal(inputKey);
// 第二轮HMAC:使用临时密钥处理信息字段
Mac hmac2 = Mac.getInstance(HMAC_ALGORITHM);
SecretKeySpec keySpec2 = new SecretKeySpec(tempKey, HMAC_ALGORITHM);
hmac2.init(keySpec2);
return hmac2.doFinal(HKDF_INFO.getBytes(StandardCharsets.UTF_8));
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException("Failed to derive key", e);
}
}令牌验证逻辑
验证过程是生成过程的逆操作,重点包含时间窗口校验和安全的令牌比较:
public static boolean verifyToken(long timestamp, String nonce, String clientId, String receivedToken) {
// 1. 时间窗口校验:超出5分钟直接拒绝
long currentTime = Instant.now().getEpochSecond();
if (Math.abs(currentTime - timestamp) > TIME_WINDOW_SECONDS) {
return false;
}
// 2. 重新生成令牌
String message = String.join("_",
String.valueOf(timestamp),
nonce,
clientId != null ? clientId : ""
);
byte[] derivedKey = deriveKey((clientId != null ? clientId : "").getBytes(StandardCharsets.UTF_8));
String expectedToken = hmacDigest(derivedKey, message);
// 3. 安全比较:防止时序攻击
return secureCompare(expectedToken, receivedToken);
}安全比较方法
普通的字符串 equals 方法存在时序攻击风险,我们实现了恒定时间的比较方法:
private static boolean secureCompare(String a, String b) {
if (a == null || b == null) {
return false;
}
if (a.length() != b.length()) {
return false;
}
int result = 0;
// 遍历所有字符,确保比较时间恒定
for (int i = 0; i < a.length(); i++) {
result |= a.charAt(i) ^ b.charAt(i);
}
return result == 0;
}请求头集成
在实际的 HTTP 请求中,我们需要将生成的令牌信息添加到请求头中:
// 执行在发送请求前
req.addHeader("timestamp", authToken.getTimestamp()); // 添加时间戳
req.addHeader("nonce", authToken.getNonce()); // 添加随机数
req.addHeader("clientId", deviceSerial); // 添加客户端ID
req.addHeader("token", authToken.getToken()); // 添加签名令牌完整使用示例
public static void main(String[] args) {
// 1. 生成令牌(传入客户端唯一标识)
AuthToken authToken = generateToken("2b0ad3b668d44fa4");
// 2. 模拟验证令牌
boolean isValid = verifyToken(
authToken.getTimestamp(),
authToken.getNonce(),
"2b0ad3b668d44fa4",
authToken.getToken()
);
System.out.println("令牌验证结果:" + isValid); // 输出 true
}总结
- 该 API 安全认证方案基于 HMAC-SHA256 实现,核心通过时间戳 + 随机 Nonce + 密钥派生 + 签名验证四重机制保障接口安全
- 方案重点解决了重放攻击、请求篡改和时序攻击三大安全问题,同时兼顾性能和易用性
- 实际应用中需注意密钥的安全管理、HTTPS 传输和请求频率限制等配套安全措施,进一步提升整体安全性