当前位置:首页 > 文章列表 > 文章 > java教程 > Java Webhook 签名校验实战:用时间戳和 HMAC 阻断重放请求

Java Webhook 签名校验实战:用时间戳和 HMAC 阻断重放请求

来源:17golang原创 2026-07-26 14:53:14 0浏览 收藏

订单系统收到第三方 Webhook 后,最麻烦的情况不是接口返回 500,而是同一笔回调隔几分钟又来一次,业务却已经执行过。只把请求头里的密钥比对一下,无法区分“刚发来的请求”和“被截获后重新发送的旧请求”。

一个可落地的校验组合是:用原始请求体参与 HMAC-SHA256 签名,再把时间戳放进签名输入,并限制请求只能在短时间窗口内到达。验签通过后还要用业务事件 ID 做幂等处理。

要点速览

  • 签名必须基于原始请求体,不能基于解析后重新拼接的 JSON。
  • 时间戳负责缩短重放窗口,业务事件 ID 负责避免重复落库。
  • 比较签名时使用 MessageDigest.isEqual,避免普通字符串比较带来的时序差异。
  • 验收至少覆盖正常请求、过期请求和请求体被改动三种结果。

先做一个能验收的 Webhook 小服务

为了把边界讲清楚,示例只保留一个回调入口。请求方发送三个请求头:X-Webhook-TimestampX-Webhook-IdX-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 反序列化成对象,再用对象重新序列化去签名;字段顺序、空格和转义方式稍有不同,合法请求也会验签失败。

Java Webhook 签名字段从原始请求体和时间戳组成 HMAC 后得到通过或拒绝结果

用 HMAC-SHA256 校验原始请求体

Java 标准库已经提供了 MacSecretKeySpec,不需要自己实现哈希算法。生产代码应从密钥管理系统读取密钥;为了方便本地运行,下面把密钥作为构造参数传入。

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;不要把完整请求体写入普通日志。

Java Webhook 首次请求在时间窗口内通过,过期请求和篡改请求被拒绝

把事件 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 是否已经处理。先用三组请求验收,再接入真实业务,排错时会比“回调偶尔重复”更容易定位。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go 1.24 os.Root 怎么限制文件访问:文件夹隔离、路径遍历与回归验收Go 1.24 os.Root 怎么限制文件访问:文件夹隔离、路径遍历与回归验收
上一篇
Go 1.24 os.Root 怎么限制文件访问:文件夹隔离、路径遍历与回归验收
AI 流式输出总是半截 JSON:用 Go 增量缓冲器拼出完整工具参数
下一篇
AI 流式输出总是半截 JSON:用 Go 增量缓冲器拼出完整工具参数
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    120次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    41次使用
  • OpenCompass大模型评测体系详解:功能、使用指南与应用场景
    OpenCompass
    OpenCompass是上海AI实验室推出的开源大模型评测平台,提供CompassKit、CompassHub和CompassRank三大核心组件,支持LLM及多模态模型的一站式标准化评估与排行榜查询。
    62次使用
  • AGI-Eval大模型评测平台:权威榜单、数据集与人机协同评测方案
    AGI-Eval
    AGI-Eval是由上海交大等高校联合发布的大模型评测社区,提供公正透明的LLM能力榜单、多领域评测集及Data Studio数据服务,助力AI模型性能评估与NLP科研开发。
    40次使用
  • SuperCLUE中文大模型评测基准:功能、能力维度与应用指南
    SuperCLUE
    SuperCLUE是权威的中文大语言模型综合评测基准,涵盖语言理解、知识应用、AI Agent智能体及安全性等12项核心能力。通过多轮对话与客观测试,定期发布榜单与技术报告,为模型研发、优化及行业选型提供科学依据。
    275次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码