别再让API裸奔了!Spring Boot实战:5分钟给你的登录接口加上防重放攻击(时间戳+序列号)
Spring Boot实战5分钟构建防重放攻击的登录接口在移动支付和在线服务普及的今天API安全已成为开发者必须直面的挑战。想象这样一个场景用户A在咖啡店使用公共Wi-Fi登录电商平台攻击者只需抓取登录请求数据包就能在任意时间重复发送相同请求轻松冒充用户身份——这就是典型的重放攻击Replay Attack。不同于需要破解加密算法的复杂攻击手段重放攻击仅需复制原始数据即可实施对缺乏防护的API堪称降维打击。1. 防重放攻击核心策略对比防重放攻击的本质是确保每个请求的唯一性和时效性。主流方案中时间戳与序列号组合被证明是平衡实现成本与安全性的优选方案。我们先通过对比表理解关键差异策略类型实现复杂度存储开销适用场景典型容差时间纯时间戳★★☆无低频敏感操作1-5分钟纯序列号★★★★高金融级安全场景无时间戳序列号★★★低通用业务场景3-5分钟提示选择方案时需考虑业务容忍度。例如支付接口建议采用序列号机制而普通登录接口用时间戳即可满足需求。2. 时间戳拦截器实战让我们从零实现一个基于时间戳的Spring Boot拦截器。这种方案的优势在于零存储依赖仅需客户端与服务端保持基本时间同步。2.1 创建时间戳校验注解首先定义方法级注解标记需要防重放的接口Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface TimestampValidation { long tolerance() default 300000; // 默认5分钟容差 }2.2 实现拦截器逻辑核心校验逻辑集中在拦截器的preHandle方法public class TimestampInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Method method ((HandlerMethod) handler).getMethod(); TimestampValidation annotation method.getAnnotation(TimestampValidation.class); if (annotation ! null) { String timestampStr request.getHeader(X-TIMESTAMP); if (StringUtils.isEmpty(timestampStr)) { response.sendError(400, Missing timestamp header); return false; } long clientTime Long.parseLong(timestampStr); long serverTime System.currentTimeMillis(); if (Math.abs(serverTime - clientTime) annotation.tolerance()) { response.sendError(403, Request expired); return false; } } return true; } }2.3 注册拦截器配置通过配置类启用拦截器Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new TimestampInterceptor()) .addPathPatterns(/api/**); } }实际应用时前端需要在请求头添加当前时间戳curl -X POST http://localhost:8080/api/login \ -H X-TIMESTAMP: $(date %s000) \ -d {username:test,password:123456}3. 序列号防御进阶方案对于安全等级要求更高的场景我们需要引入序列号Nonce机制。其核心原理是服务端记录已使用的序列号拒绝重复请求。3.1 Redis存储方案设计使用Redis的原子性操作实现高效校验public class NonceService { private final RedisTemplateString, String redisTemplate; public boolean validateNonce(String nonce, long timestamp) { String key nonce: nonce; // 原子性设置key存在则返回false Boolean result redisTemplate.opsForValue() .setIfAbsent(key, 1, Duration.ofMinutes(5)); return Boolean.TRUE.equals(result); } }3.2 增强版拦截器实现在时间戳校验基础上增加Nonce验证Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String nonce request.getHeader(X-NONCE); String timestamp request.getHeader(X-TIMESTAMP); if (!nonceService.validateNonce(nonce, Long.parseLong(timestamp))) { response.sendError(403, Duplicate request); return false; } return super.preHandle(request, response, handler); }客户端需要保证每次请求生成唯一Noncefunction generateNonce() { return crypto.randomUUID(); // 或使用其他加密安全随机数 }4. 生产环境优化策略基础方案落地后这些优化点能进一步提升防御效果4.1 时钟漂移补偿解决客户端与服务端时间不同步问题// 在拦截器中添加时间补偿逻辑 long timeDiff getAverageTimeDiff(clientIp); long adjustedServerTime System.currentTimeMillis() timeDiff;4.2 请求签名机制防止请求参数被篡改String signature HMAC_SHA256(secretKey, method url timestamp nonce body);验证流程校验时间戳有效性验证Nonce唯一性核对签名一致性执行业务逻辑4.3 限流保护结合Guava RateLimiter防止暴力重放private final RateLimiter limiter RateLimiter.create(10.0); // 10QPS if (!limiter.tryAcquire()) { throw new RateLimitException(); }5. 方案选型决策树根据你的业务特征选择合适的防护等级graph TD A[开始] -- B{是否金融级应用?} B --|是| C[时间戳Nonce签名限流] B --|否| D{是否用户敏感操作?} D --|是| E[时间戳简单签名] D --|否| F[基础时间戳校验]实际项目中我在处理支付系统时曾遇到一个典型案例攻击者利用重放请求在夜间批量执行小额转账。后来通过引入Nonce机制配合异地登录检测成功将此类攻击归零。关键是要记住——没有绝对安全的系统但每增加一层防御攻击成本就呈指数级上升。