API接口签名验证实战
API接口签名验证实战
一、接口签名概述
API签名验证是保护接口安全的重要手段,防止请求被篡改或伪造。
1.1 签名机制原理
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ 客户端 │ │ 网络 │ │ 服务端 │ ├─────────────────┤ ├─────────────────┤ ├─────────────────┤ │ 1. 构造请求参数 │────▶│ 传输请求 │────▶│ 1. 获取请求参数 │ │ 2. 生成签名 │ │ │ │ 2. 重新生成签名 │ │ 3. 发送请求 │ │ │ │ 3. 验证签名 │ └─────────────────┘ └─────────────────┘ │ 4. 处理请求 │ └─────────────────┘1.2 签名要素
| 要素 | 说明 | 示例 |
|---|---|---|
| AppKey | 应用标识 | app001 |
| AppSecret | 应用密钥 | xxxxxx |
| Timestamp | 时间戳 | 1704067200 |
| Nonce | 随机数 | abc123 |
| Signature | 签名结果 | md5(xxx) |
二、签名算法实现
2.1 签名生成流程
public class SignatureUtil { private static final String SIGN_METHOD = "MD5"; private static final String CHARSET = "UTF-8"; public static String generateSignature(Map<String, String> params, String appSecret) { // 1. 去除sign参数 params.remove("sign"); // 2. 按字典序排序 List<String> keys = new ArrayList<>(params.keySet()); Collections.sort(keys); // 3. 拼接参数 StringBuilder sb = new StringBuilder(); for (String key : keys) { String value = params.get(key); if (value != null && !value.isEmpty()) { sb.append(key).append("=").append(value).append("&"); } } // 4. 拼接密钥 sb.append("key=").append(appSecret); // 5. 计算签名 return md5(sb.toString()); } private static String md5(String input) { try { MessageDigest md = MessageDigest.getInstance(SIGN_METHOD); byte[] digest = md.digest(input.getBytes(CHARSET)); StringBuilder sb = new StringBuilder(); for (byte b : digest) { sb.append(String.format("%02x", b)); } return sb.toString().toUpperCase(); } catch (Exception e) { throw new RuntimeException("MD5 calculation failed", e); } } }2.2 HMAC-SHA256签名
public class HmacSignatureUtil { private static final String ALGORITHM = "HmacSHA256"; public static String generateHmacSignature(Map<String, String> params, String appSecret) { params.remove("sign"); List<String> keys = new ArrayList<>(params.keySet()); Collections.sort(keys); StringBuilder sb = new StringBuilder(); for (String key : keys) { String value = params.get(key); if (value != null && !value.isEmpty()) { sb.append(key).append("=").append(value).append("&"); } } sb.append("key=").append(appSecret); try { Mac mac = Mac.getInstance(ALGORITHM); SecretKeySpec keySpec = new SecretKeySpec(appSecret.getBytes(), ALGORITHM); mac.init(keySpec); byte[] digest = mac.doFinal(sb.toString().getBytes()); return Base64.getEncoder().encodeToString(digest); } catch (Exception e) { throw new RuntimeException("HMAC calculation failed", e); } } }三、签名验证实现
3.1 验证过滤器
public class SignatureFilter implements Filter { private static final long TIMESTAMP_TOLERANCE = 5 * 60 * 1000; // 5分钟 @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; try { // 1. 获取参数 Map<String, String> params = getRequestParams(httpRequest); // 2. 验证时间戳 String timestamp = params.get("timestamp"); validateTimestamp(timestamp); // 3. 验证随机数 String nonce = params.get("nonce"); validateNonce(nonce); // 4. 获取AppKey String appKey = params.get("appKey"); String appSecret = getAppSecret(appKey); // 5. 验证签名 String sign = params.get("sign"); String expectedSign = SignatureUtil.generateSignature(params, appSecret); if (!sign.equalsIgnoreCase(expectedSign)) { httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); httpResponse.getWriter().write("Invalid signature"); return; } chain.doFilter(request, response); } catch (Exception e) { httpResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST); } } private void validateTimestamp(String timestamp) { long ts = Long.parseLong(timestamp); long now = System.currentTimeMillis() / 1000; if (Math.abs(now - ts) > TIMESTAMP_TOLERANCE / 1000) { throw new RuntimeException("Invalid timestamp"); } } private void validateNonce(String nonce) { if (nonce == null || nonce.length() < 8) { throw new RuntimeException("Invalid nonce"); } } }3.2 Nonce去重
public class NonceManager { private final StringRedisTemplate redisTemplate; private static final String PREFIX = "nonce:"; private static final long EXPIRE_SECONDS = 5 * 60; // 5分钟 public void validateNonce(String nonce) { String key = PREFIX + nonce; Boolean exists = redisTemplate.hasKey(key); if (Boolean.TRUE.equals(exists)) { throw new RuntimeException("Duplicate nonce"); } redisTemplate.opsForValue().set(key, "true", EXPIRE_SECONDS, TimeUnit.SECONDS); } }四、请求示例
4.1 客户端请求
public class ApiClient { private String appKey = "app001"; private String appSecret = "secret123"; public String sendRequest(String url, Map<String, Object> data) { Map<String, String> params = new HashMap<>(); params.put("appKey", appKey); params.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000)); params.put("nonce", UUID.randomUUID().toString().replace("-", "").substring(0, 16)); for (Map.Entry<String, Object> entry : data.entrySet()) { params.put(entry.getKey(), String.valueOf(entry.getValue())); } String sign = SignatureUtil.generateSignature(params, appSecret); params.put("sign", sign); // 发送请求... return doPost(url, params); } }4.2 curl示例
curl -X POST http://api.example.com/endpoint \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "appKey=app001" \ -d "timestamp=1704067200" \ -d "nonce=abc123def456" \ -d "data=test" \ -d "sign=A1B2C3D4E5F6"五、安全加固
5.1 使用HTTPS
server: ssl: enabled: true key-store: classpath:keystore.p12 key-store-password: password key-store-type: PKCS12 key-alias: tomcat5.2 IP白名单
public class IpWhitelistFilter implements Filter { private Set<String> whitelist = Set.of( "192.168.1.1", "10.0.0.0/8" ); @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { String clientIp = getClientIp((HttpServletRequest) request); if (!isIpWhitelisted(clientIp)) { ((HttpServletResponse) response).setStatus(HttpServletResponse.SC_FORBIDDEN); return; } chain.doFilter(request, response); } }5.3 频率限制
public class RateLimitFilter implements Filter { private final StringRedisTemplate redisTemplate; private static final int MAX_REQUESTS = 100; private static final int TIME_WINDOW_SECONDS = 60; @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { String clientIp = getClientIp((HttpServletRequest) request); String key = "rate_limit:" + clientIp; Long count = redisTemplate.opsForValue().increment(key); if (count == 1) { redisTemplate.expire(key, TIME_WINDOW_SECONDS, TimeUnit.SECONDS); } if (count > MAX_REQUESTS) { ((HttpServletResponse) response).setStatus(429); return; } chain.doFilter(request, response); } }六、签名算法对比
| 算法 | 安全性 | 性能 | 适用场景 |
|---|---|---|---|
| MD5 | 低 | 高 | 内部系统 |
| SHA-1 | 中 | 高 | 一般场景 |
| SHA-256 | 高 | 中 | 重要场景 |
| HMAC-SHA256 | 高 | 中 | 对外API |
七、最佳实践
7.1 签名规范
- 参数排序:按字典序升序排列
- 空值处理:忽略空参数
- 编码格式:统一使用UTF-8
- 签名格式:统一大写或小写
7.2 安全建议
- 密钥管理:使用密钥管理服务存储密钥
- 定期轮换:定期更换AppSecret
- 日志审计:记录签名验证失败日志
- 异常监控:监控异常签名请求
7.3 常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 签名不一致 | 参数顺序不同 | 统一排序规则 |
| 时间戳过期 | 客户端时间不准 | 增加时间容错 |
| Nonce重复 | 请求重放 | 实现Nonce去重 |
八、总结
API签名验证是保护接口安全的重要手段:
- 选择合适算法:根据安全需求选择HMAC-SHA256
- 实现完整验证:时间戳、Nonce、签名缺一不可
- 配合其他措施:HTTPS、IP白名单、频率限制
- 做好密钥管理:使用安全的密钥存储方案
通过以上措施,可以有效防止接口被篡改和重放攻击。
