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 签名生成流程

java 复制代码
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签名

java 复制代码
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 验证过滤器

java 复制代码
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去重

java 复制代码
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 客户端请求

java 复制代码
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示例

bash 复制代码
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

yaml 复制代码
server:
  ssl:
    enabled: true
    key-store: classpath:keystore.p12
    key-store-password: password
    key-store-type: PKCS12
    key-alias: tomcat

5.2 IP白名单

java 复制代码
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 频率限制

java 复制代码
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 签名规范

  1. 参数排序:按字典序升序排列
  2. 空值处理:忽略空参数
  3. 编码格式:统一使用UTF-8
  4. 签名格式:统一大写或小写

7.2 安全建议

  1. 密钥管理:使用密钥管理服务存储密钥
  2. 定期轮换:定期更换AppSecret
  3. 日志审计:记录签名验证失败日志
  4. 异常监控:监控异常签名请求

7.3 常见问题

问题 原因 解决方案
签名不一致 参数顺序不同 统一排序规则
时间戳过期 客户端时间不准 增加时间容错
Nonce重复 请求重放 实现Nonce去重

八、总结

API签名验证是保护接口安全的重要手段:

  1. 选择合适算法:根据安全需求选择HMAC-SHA256
  2. 实现完整验证:时间戳、Nonce、签名缺一不可
  3. 配合其他措施:HTTPS、IP白名单、频率限制
  4. 做好密钥管理:使用安全的密钥存储方案

通过以上措施,可以有效防止接口被篡改和重放攻击。

相关推荐
键盘歌唱家17 分钟前
Spring AI 入门分享:它和“直接调 API“到底差在哪
java·人工智能·spring
宸丶一1 小时前
Day 10:LangGraph - Agent 的图执行引擎
java·windows·python
hikktn1 小时前
Excel 导出 OOM 预防实战:30 万行从堆溢出到 50MB 的演进
java·excel·easyexcel
风味蘑菇干1 小时前
WTomcat服务器
java·服务器
燕-孑1 小时前
tomcat详解(基础到高级生产)
java·tomcat
码不停蹄的玄黓1 小时前
Spring Bean 生命周期
java·后端·spring
西安邮电大学2 小时前
分治算法详细讲解
java·后端·其他·算法·面试
摇滚侠2 小时前
Mybatis 入门到项目实战 搭建 MyBatis 框架 01-14
java·tomcat·mybatis
cfm_29142 小时前
JVM底层源码深度解析:读写屏障(Read/Write Barrier)
jvm
码不停蹄的玄黓2 小时前
SpringBoot 全局异常处理器实现
java·spring boot·后端