当前位置: 首页 > news >正文

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之上,以防止传输过程中的参数被窃听或篡改。

http://www.jsqmd.com/news/687280/

相关文章:

  • 从脑电波到股票预测:变分模态分解(VMD)在Python里的3个实战应用
  • iOS 16透明小组件开发避坑指南:精准适配iPhone 14 Pro Max等全机型坐标
  • 2026年大盘点大型平板式速冻隧道项目承接厂家,怎么选择合适的? - mypinpai
  • 别再用手掰了!PCB邮票孔设计的5个实用技巧与常见避坑指南
  • 别再只抄推荐电路了!手把手教你为语音模块搭配合适的功放(以SC8002/TPA3110为例)
  • Docker 27边缘节点编排失效全复盘(27.0.1→27.2.0升级血泪教训)
  • 聊聊做大型储藏冷库项目经验足的厂家,哪家性价比高 - 工业品网
  • 如何彻底解决电脑风扇噪音问题:FanControl风扇控制软件终极指南
  • Mac上玩转软件无线电:保姆级VMware Fusion虚拟机安装Ubuntu 20.04并配置GNU Radio 3.10全记录
  • 说说荣程制冷设备性价比,河北、内蒙古等地使用它费用贵吗? - 工业推荐榜
  • 从Spyglass到VC Spyglass:一个EDA工具的进化史,以及它如何用机器学习搞定CDC验证
  • SCP:单细胞数据分析管道的多维技术解析与实战指南
  • 鸿蒙中 Account Kit:一键登录、华为账号登录、静默登录(一)
  • 别再让你的模型输出NaN了!用LogSumExp技巧搞定Softmax数值溢出(附PyTorch/TensorFlow代码)
  • 实战React Flow Renderer(一):从零搭建可拖拽低代码流程图编辑器
  • 江苏威昊流体科技性价比高吗?服务质量如何? - 工业设备
  • 美术说动画滑步,技术说包体爆炸?给Unity团队的AnimationClip优化协作指南
  • GPT Image 2 提示词指南
  • 经验丰富的储藏冷库工程厂家选择要点有哪些 - mypinpai
  • 保姆级教程:在Ubuntu 20.04上用Qt 5.12.8从源码编译QGC地面站(附常见编译错误解决)
  • 告别Makefile恐惧症:手把手教你用VCS常用参数搭建可复用的仿真脚本模板
  • 避开封号风险:手把手教你用YOLOv5在本地搭建FPS游戏目标检测实验环境(附CSGO数据集)
  • 免费开源的Windows桌面分区神器:NoFences让你的桌面焕然一新
  • PL2303老芯片Windows 10/11驱动终极解决方案:三步让老旧串口设备重获新生
  • 抖音直播回放下载终极指南:快速保存精彩直播的免费工具实战
  • Proteus仿真ADC0832与51单片机通信:一个被忽视的硬件SPI替代方案
  • 东南亚服装产业自动化转型:激光开袋机的市场现状与中国品牌出海实践
  • 2026年速冻隧道制冷机组专业生产厂家,好用品牌排行榜出炉 - 工业品网
  • Obsidian模板终极指南:如何用16个模板建立你的第二大脑
  • 智能电表抄表协议DL/T645和698.45,到底有啥区别?一个项目实战讲清楚