Java Webhook 签名校验实战:用时间戳和 HMAC 阻断重放请求
订单系统收到第三方 Webhook 后,最麻烦的情况不是接口返回 500,而是同一笔回调隔几分钟又来一次,业务却已经执行过。只把请求头里的密钥比对一下,无法区分“刚发来的请求”和“被截获后重新发送的旧请求”。
一个可落地的校验组合是:用原始请求体参与 HMAC-SHA256 签名,再把时间戳放进签名输入,并限制请求只能在短时间窗口内到达。验签通过后还要用业务事件 ID 做幂等处理。
要点速览
- 签名必须基于原始请求体,不能基于解析后重新拼接的 JSON。
- 时间戳负责缩短重放窗口,业务事件 ID 负责避免重复落库。
- 比较签名时使用 MessageDigest.isEqual,避免普通字符串比较带来的时序差异。
- 验收至少覆盖正常请求、过期请求和请求体被改动三种结果。
先做一个能验收的 Webhook 小服务
为了把边界讲清楚,示例只保留一个回调入口。请求方发送三个请求头:X-Webhook-Timestamp、X-Webhook-Id 和 X-Webhook-Signature。签名原文采用 时间戳 + "." + 原始请求体,再用共享密钥计算 HMAC-SHA256。
String signingText = timestamp + "." + rawBody;
String signature = hmacSha256Hex(secret, signingText);
if (!sameSignature(signature, receivedSignature)) {
return ResponseEntity.status(401).body("signature rejected");
}
return ResponseEntity.ok("accepted");
这里的 rawBody 是 HTTP 请求刚读出的字节转成的 UTF-8 文本。不要先把 JSON 反序列化成对象,再用对象重新序列化去签名;字段顺序、空格和转义方式稍有不同,合法请求也会验签失败。

用 HMAC-SHA256 校验原始请求体
Java 标准库已经提供了 Mac 和 SecretKeySpec,不需要自己实现哈希算法。生产代码应从密钥管理系统读取密钥;为了方便本地运行,下面把密钥作为构造参数传入。
private static String hmacSha256Hex(String secret, String text) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] digest = mac.doFinal(text.getBytes(StandardCharsets.UTF_8));
StringBuilder hex = new StringBuilder(digest.length * 2);
for (byte value : digest) {
hex.append(String.format("%02x", value));
}
return hex.toString();
} catch (GeneralSecurityException e) {
throw new IllegalStateException("cannot build webhook signature", e);
}
}
private static boolean sameSignature(String expected, String actual) {
if (expected == null || actual == null) {
return false;
}
return MessageDigest.isEqual(
expected.getBytes(StandardCharsets.US_ASCII),
actual.getBytes(StandardCharsets.US_ASCII));
}
验签函数只负责判断签名是否一致,不负责确认事件是否处理过。两件事拆开后,密钥轮换、错误统计和业务幂等都更容易测试。
再加时间窗口,拦住旧请求重放
攻击者即使拿到一份完整的合法请求,也不应该在很久以后再次提交。服务端读取时间戳后,先计算它与本机当前时间的差值。示例把允许窗口设为 300 秒;实际值要结合第三方的重试策略和网络延迟调整。
private static boolean withinWindow(String timestamp, long nowSeconds) {
try {
long sentAt = Long.parseLong(timestamp);
return Math.abs(nowSeconds - sentAt)
校验顺序建议是先检查时间戳格式和窗口,再计算签名,最后进入业务幂等判断。窗口过期时直接返回明确的 401 或 400,并记录事件 ID;不要把完整请求体写入普通日志。

把事件 ID 接到幂等处理上
签名正确只说明请求来自持有密钥的一方,不代表它是第一次到达。收到 X-Webhook-Id 后,可以在数据库建立唯一索引,例如 webhook_event(event_id)。插入成功才执行业务;唯一键冲突时返回已处理状态。
CREATE TABLE webhook_event (
event_id VARCHAR(80) PRIMARY KEY,
received_at TIMESTAMP NOT NULL,
payload_hash CHAR(64) NOT NULL,
status VARCHAR(20) NOT NULL
);
这样即使第三方在网络超时后重试,服务也不会把同一个事件再次转成订单动作。若业务必须区分“处理中”和“已完成”,把状态更新放在同一事务边界内,并为长时间卡住的处理中记录准备人工或定时补偿路径。
用三组请求确认结果
本地验收时固定请求体和事件 ID,先生成签名,再分别改变时间戳或 JSON 内容。不要只测试一条成功请求,那只能证明代码能走通,不能证明边界有效。
body='{"eventId":"evt_1001","type":"order.paid"}'
ts=$(date +%s)
signature=$(./sign-webhook "$WEBHOOK_SECRET" "$ts.$body")
curl -i http://localhost:8080/webhook \
-H "Content-Type: application/json" \
-H "X-Webhook-Timestamp: $ts" \
-H "X-Webhook-Id: evt_1001" \
-H "X-Webhook-Signature: $signature" \
--data "$body"
- 正常请求:返回 200,事件表新增
evt_1001。 - 重复请求:签名仍然正确,但唯一键命中,业务动作不再执行。
- 过期请求:返回 401,日志只保留事件 ID、失败原因和时间差。
- 篡改请求:保持旧签名但修改
type,返回 401。
常见问题
为什么不能只比较一个固定 Authorization?
固定密钥能证明请求方知道秘密,却不能证明请求是新鲜的。时间戳和请求体一起参与签名,才能让旧报文在窗口之外失效。
JSON 空格变化会导致验签失败吗?
会。签名针对的是原始字节序列,所以服务端应在解析 JSON 前完成验签。
时间窗口设得越短越安全吗?
不一定。窗口过短会误伤网络抖动和第三方重试;应根据真实延迟指标设置,并配合事件 ID 幂等。
验签通过后还需要做什么?
还要校验事件类型、字段范围、事件 ID 唯一性和业务状态。密码学通过不是业务数据可信的全部证明。
小结
这个小服务的核心不是某个框架注解,而是把四个判断按顺序固定下来:原始请求体是否完整、时间戳是否在窗口内、HMAC 是否匹配、事件 ID 是否已经处理。先用三组请求验收,再接入真实业务,排错时会比“回调偶尔重复”更容易定位。
Go 1.24 os.Root 怎么限制文件访问:文件夹隔离、路径遍历与回归验收
- 上一篇
- Go 1.24 os.Root 怎么限制文件访问:文件夹隔离、路径遍历与回归验收
- 下一篇
- AI 流式输出总是半截 JSON:用 Go 增量缓冲器拼出完整工具参数
-
- 文章 · java教程 | 12小时前 |
- ServiceLoader provider怎么配置或排查
- 488浏览 收藏
-
- 文章 · java教程 | 13小时前 |
- MethodHandle 类型怎么配置或排查
- 214浏览 收藏
-
- 文章 · java教程 | 14小时前 | nio · 故障排查 · Java教程 · ByteBuffer · java limit position ByteBuffer flip
- ByteBuffer flip 状态怎么配置或排查
- 475浏览 收藏
-
- 文章 · java教程 | 15小时前 |
- Files.walk 关闭怎么配置或排查
- 347浏览 收藏
-
- 文章 · java教程 | 18小时前 |
- Stream toList 不可变怎么配置或排查
- 127浏览 收藏
-
- 文章 · java教程 | 19小时前 | Java · 异常处理 · 并发编程 · completablefuture Java异步
- CompletableFuture 异常阶段怎么配置或排查
- 265浏览 收藏
-
- 文章 · java教程 | 20小时前 | Java · 并发排查 · ScopedValue · java 上下文 并发 ScopedValue
- Scoped Values 上下文怎么配置或排查
- 107浏览 收藏
-
- 文章 · java教程 | 22小时前 |
- Virtual Thread pinning怎么配置或排查
- 381浏览 收藏
-
- 文章 · java教程 | 23小时前 |
- switch pattern null怎么配置或排查
- 198浏览 收藏
-
- 文章 · java教程 | 1天前 | Java · 排查 · 测试覆盖率 · maven JaCoCo sealed branch coverage
- sealed class 分支覆盖怎么配置或排查
- 289浏览 收藏
-
- 文章 · java教程 | 1天前 |
- Record 可变集合怎么配置或排查
- 359浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 120次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 41次使用
-
- OpenCompass
- OpenCompass是上海AI实验室推出的开源大模型评测平台,提供CompassKit、CompassHub和CompassRank三大核心组件,支持LLM及多模态模型的一站式标准化评估与排行榜查询。
- 62次使用
-
- AGI-Eval
- AGI-Eval是由上海交大等高校联合发布的大模型评测社区,提供公正透明的LLM能力榜单、多领域评测集及Data Studio数据服务,助力AI模型性能评估与NLP科研开发。
- 40次使用
-
- SuperCLUE
- SuperCLUE是权威的中文大语言模型综合评测基准,涵盖语言理解、知识应用、AI Agent智能体及安全性等12项核心能力。通过多轮对话与客观测试,定期发布榜单与技术报告,为模型研发、优化及行业选型提供科学依据。
- 275次使用
-
- Java try-with-resources 多个资源关闭顺序是什么
- 2026-09-10 501浏览
-
- 矩阵主副对角线快速定位技巧
- 2026-05-31 501浏览
-
- Java多态优化流程代码与行为分发改进
- 2026-05-26 501浏览
-
- JVM 类元数据双亲委派链表深度解析
- 2026-05-21 501浏览
-
- 反射异常处理:InvocationTargetException解析与应用
- 2026-05-16 501浏览

