news 2026/4/23 12:50:18

Spring Boot 实现接口防止重放攻击验证(时间戳 + 随机数 + 签名)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot 实现接口防止重放攻击验证(时间戳 + 随机数 + 签名)

全栈客

没有奇迹,只有你努力的轨迹;没有运气,只有你坚持的勇气。

101篇原创内容

公众号

实现思路

在请求头Header或参数中携带timestamp(通常是 13 位毫秒级时间戳)、签名Signature与 随机数Nonce,服务端校验该时间戳、随机数和签名,则判定请求过期或无效。

机制

作用

实现简述

时间戳

限制请求有效期

如上所述,超过 5 分钟即失效。

签名

防篡改、防伪造

将参数 + 时间戳 + 密钥 进行MD5/SHA256加密,服务端重新计算比对。

Nonce

防窗口期内重放

即使攻击者在 5 分钟内截获请求并重放,由于Nonce只能使用一次(需配合 Redis 缓存校验),请求也会被拒绝。

核心在于构建一个“时间戳 + 随机数 + 签名”的三重验证体系,这能有效防止请求被截获后重复提交。

三重防护

  • • 时间戳 (Timestamp):为请求设置一个“有效期”(例如5分钟)。服务器收到请求后,会校验请求时间与服务器时间的差值。如果超出有效期,请求直接被视为过期,这抵御了长期的重放攻击。

  • • 随机数 (Nonce):一个全局唯一的随机字符串,代表“一次性有效”。服务器会检查在时间戳的有效期内,这个Nonce是否已经被使用过。如果已存在,则判定为重放攻击,这抵御了短期内的重放攻击。

  • • 签名 (Signature):将业务参数、时间戳、随机数以及一个只有客户端和服务端知道的密钥(AppSecret)按约定规则拼接后,进行加密(如HMAC-SHA256)生成。这确保了请求参数在传输过程中未被篡改。

完整案例

客户端:生成请求签名

客户端在发起请求前,需要生成签名相关的参数并放入请求头。

import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.*; import java.util.Base64; public class ClientSignUtil { private static final String APP_KEY = "your_app_key"; private static final String APP_SECRET = "your_app_secret"; // 密钥绝不能泄露 private static final String SIGN_ALGORITHM = "HmacSHA256"; /** * 生成签名所需的请求头参数 * @param businessParams 业务参数Map * @return 包含 appKey, timestamp, nonce, sign 的Map */ public static Map<String, String> generateRequestHeaders(Map<String, Object> businessParams) { // 生成时间戳(毫秒) long timestamp = System.currentTimeMillis(); // 生成随机数 Nonce String nonce = UUID.randomUUID().toString().replace("-", ""); // 准备参与签名的参数 Map<String, Object> signParams = new TreeMap<>(businessParams); // TreeMap会自动按key排序 signParams.put("appKey", APP_KEY); signParams.put("timestamp", timestamp); signParams.put("nonce", nonce); // 计算签名 String sign = calculateSignature(signParams, APP_SECRET); // 组装请求头 Map<String, String> headers = new HashMap<>(); headers.put("X-App-Key", APP_KEY); headers.put("X-Timestamp", String.valueOf(timestamp)); headers.put("X-Nonce", nonce); headers.put("X-Signature", sign); return headers; } /** * 计算签名 (HMAC-SHA256) */ private static String calculateSignature(Map<String, Object> params, String secret) { try { // 拼接签名字符串: key1=value1&key2=value2...+secret StringBuilder sb = new StringBuilder(); for (Map.Entry<String, Object> entry : params.entrySet()) { if (entry.getValue() != null) { sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&"); } } sb.append(secret); // 在末尾拼接密钥 // 使用 HMAC-SHA256 算法进行加密 Mac mac = Mac.getInstance("HmacSHA256"); SecretKeySpec secretKeySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"); mac.init(secretKeySpec); byte[] rawHmac = mac.doFinal(sb.toString().getBytes(StandardCharsets.UTF_8)); // 使用URL安全的Base64编码 return Base64.getUrlEncoder().withoutPadding().encodeToString(rawHmac); } catch (NoSuchAlgorithmException | InvalidKeyException e) { throw new RuntimeException("签名生成失败", e); } } }

服务端:校验签名与防重放

服务端通过一个拦截器在请求到达业务逻辑前进行统一校验

防重放工具类ReplayAttackUtils.java

import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import java.util.concurrent.TimeUnit; @Component public class ReplayAttackUtils { private final StringRedisTemplate redisTemplate; private static final String NONCE_KEY_PREFIX = "api:security:nonce:"; // 时间窗口,与客户端约定的有效期一致,例如5分钟(300秒) private static final long TIME_WINDOW_SECONDS = 300L; public ReplayAttackUtils(StringRedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } /** * 校验请求是否为重放请求 * @param nonce 请求中的随机数 * @param timestamp 请求中的时间戳(秒) * @return true-非重放,可继续处理;false-重放请求,应拒绝 */ public boolean checkReplayAttack(String nonce, long timestamp) { if (!StringUtils.hasText(nonce)) { return false; } // 校验时间戳 long currentSeconds = System.currentTimeMillis() / 1000; long timeDiff = Math.abs(currentSeconds - timestamp); if (timeDiff > TIME_WINDOW_SECONDS) { // 时间戳已过期 return false; } // 校验 Nonce (利用Redis的原子性操作 setIfAbsent) String key = NONCE_KEY_PREFIX + nonce; // 尝试设置 key,如果 key 不存在则设置成功,并设置过期时间 Boolean isAbsent = redisTemplate.opsForValue().setIfAbsent(key, "1", TIME_WINDOW_SECONDS, TimeUnit.SECONDS); // 如果返回 false,说明 key 已存在,即该 Nonce 已被使用过 return Boolean.TRUE.equals(isAbsent); } }

签名校验拦截器ApiSignatureInterceptor.java

import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.PrintWriter; import java.util.TreeMap; @Component public class ApiSignatureInterceptor implements HandlerInterceptor { private final ReplayAttackUtils replayAttackUtils; private final String APP_SECRET = "your_app_secret"; // 从配置中心或环境变量获取 public ApiSignatureInterceptor(ReplayAttackUtils replayAttackUtils) { this.replayAttackUtils = replayAttackUtils; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 从请求头获取参数 String appKey = request.getHeader("X-App-Key"); String timestampStr = request.getHeader("X-Timestamp"); String nonce = request.getHeader("X-Nonce"); String clientSignature = request.getHeader("X-Signature"); // 基础参数校验 if (!StringUtils.hasText(appKey) || !StringUtils.hasText(timestampStr) || !StringUtils.hasText(nonce) || !StringUtils.hasText(clientSignature)) { sendError(response, "缺少必要的签名参数"); return false; } long timestamp; try { timestamp = Long.parseLong(timestampStr); } catch (NumberFormatException e) { sendError(response, "时间戳格式错误"); return false; } // 防重放校验 (时间戳 + Nonce) if (!replayAttackUtils.checkReplayAttack(nonce, timestamp / 1000)) { sendError(response, "请求已过期或为重复请求"); return false; } // 签名校验 // 1、 获取所有业务参数 (这里简化处理,仅以请求参数为例) TreeMap<String, Object> signParams = new TreeMap<>(); request.getParameterMap().forEach((k, v) -> { if (v.length > 0) signParams.put(k, v[0]); }); // 2、添加签名元数据 signParams.put("appKey", appKey); signParams.put("timestamp", timestamp); signParams.put("nonce", nonce); // 3、使用同样的规则重新计算签名 String expectedSignature = calculateSignature(signParams, APP_SECRET); // 4、比对签名 (防止时序攻击,应使用常量时间比较) if (!MessageDigest.isEqual(clientSignature.getBytes(), expectedSignature.getBytes())) { sendError(response, "签名验证失败"); return false; } return true; // 校验通过,放行 } private void sendError(HttpServletResponse response, String msg) throws Exception { response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.setContentType("application/json;charset=UTF-8"); PrintWriter writer = response.getWriter(); writer.write("{\"code\": 403, \"msg\": \"" + msg + "\"}"); writer.flush(); } /** * 计算签名(和客户端的签名计算方法) */ private static String calculateSignature(Map<String, Object> params, String secret) { try { // 拼接签名字符串: key1=value1&key2=value2...+secret StringBuilder sb = new StringBuilder(); for (Map.Entry<String, Object> entry : params.entrySet()) { if (entry.getValue() != null) { sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&"); } } sb.append(secret); // 在末尾拼接密钥 // 使用HMAC-SHA256算法进行加密 Mac mac = Mac.getInstance("HmacSHA256"); SecretKeySpec secretKeySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"); mac.init(secretKeySpec); byte[] rawHmac = mac.doFinal(sb.toString().getBytes(StandardCharsets.UTF_8)); // 使用URL安全的Base64编码 return Base64.getUrlEncoder().withoutPadding().encodeToString(rawHmac); } catch (NoSuchAlgorithmException | InvalidKeyException e) { throw new RuntimeException("签名生成失败", e); } } }

注意事项

  • Nonce生成:必须保证全局唯一性和不可预测性。推荐使用UUID.randomUUID()`` 或SecureRandom` 生成,长度不低于32位。

  • Nonce存储:在分布式系统中,必须使用Redis这样的共享存储来记录已使用的Nonce。其过期时间应与时间窗口(TIME_WINDOW_SECONDS)保持一致,以节省内存。

  • • 签名比对:务必使用MessageDigest.isEqual()等常量时间比较方法,而不是简单的String.equals(),以防止时序攻击。攻击者可以通过分析不同字符串比较所花费的时间来逐字节猜测出正确的签名。

  • • 密钥安全:AppSecret是签名的核心,绝不能硬编码在代码中。应通过环境变量、配置中心或密钥管理服务(KMS)来安全地获取。

  • HTTPS:整个签名机制都应建立在HTTPS之上,以防止传输过程中的参数被窃听或篡改。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/23 12:49:20

20个大模型本体概念,产品经理必备收藏,轻松掌握AI产品决策!

本文介绍了20个大模型本体概念&#xff0c;帮助产品经理在AI产品决策中不再依赖模糊判断。文章涵盖了模型分类、开源与闭源模型、本地与云端部署、基础模型与对话模型等重要概念&#xff0c;并深入探讨了推理模型、Token计算、上下文窗口、幻觉现象、Temperature设置、System P…

作者头像 李华
网站建设 2026/4/23 12:44:11

从UI到脚本:EEGLab脑电预处理全流程实战与自动化批处理指南

1. 初识EEGLab&#xff1a;脑电预处理的基本概念与工具准备 第一次接触脑电数据分析的研究者往往会被复杂的预处理流程吓到。记得我刚开始处理脑电数据时&#xff0c;光是理解各种滤波参数就花了整整一周时间。EEGLab作为Matlab环境下最常用的脑电分析工具包&#xff0c;其优势…

作者头像 李华
网站建设 2026/4/23 12:43:21

从查重红条到 AI 零痕:9 款论文工具帮你通关毕业季

毕业季的深夜&#xff0c;谁没对着查重报告的红色波浪线叹气&#xff0c;又在 AIGC 检测页面的 “疑似度 99%” 前焦虑&#xff1f;当知网、维普的查重标准越来越严&#xff0c;AI 生成内容的检测规则持续收紧&#xff0c;论文早已不是 “写完就行” 的文字作业&#xff0c;而是…

作者头像 李华