Java将Map转回Shopify Webhook原始JSON实现HMAC校验
问题解答
核心前置说明
任何尝试将流转后的Map反向还原为Shopify原始请求体做HMAC校验的方案都无法保证100%成功率,HMAC校验对字节级一致性要求极高,只要序列化后的内容和Shopify原始发送内容存在1字节差异(比如键顺序变化、多余空格、数值格式变化、换行符差异),校验就会失败。官方推荐的标准方案是在Webhook请求入口层第一时间缓存原始请求字节流,后续业务层不管把数据转成Map还是实体类,都用缓存的原始字节做签名校验,从根源避免格式不一致问题。
1. Java端基于Map还原可校验JSON的兜底方案
如果受架构限制只能拿到流转后的Map数据,可按以下规则配置JSON序列化器,尽可能对齐Shopify的原始输出格式:
- 前置要求:业务流转过程中存储Webhook数据的Map必须使用
LinkedHashMap实现,禁止使用HashMap/TreeMap这类会打乱键插入顺序的实现,键顺序丢失的情况下完全无法还原原始格式。 - 序列化器配置(以Jackson为例,这是Java生态和Shopify序列化规则对齐度最高的JSON库):
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import java.util.Map; public class ShopifyWebhookVerifier { private static final ObjectMapper SHOPIFY_ALIGNED_MAPPER = new ObjectMapper() // 关闭格式化输出,不要添加任何缩进、换行空格 .disable(SerializationFeature.INDENT_OUTPUT) // 保留Map原有键顺序,不要自动按键名字典序排序 .disable(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS) // 不序列化值为null的键,和Shopify原始输出规则对齐 .disable(SerializationFeature.WRITE_NULL_MAP_VALUES) // 大数值不使用科学计数法输出,避免ID类字段格式错误 .enable(SerializationFeature.WRITE_BIGDECIMAL_AS_PLAIN); /** * 从Map提取用于HMAC校验的字节数组 */ public static byte[] extractVerifyBytes(Map<String, Object> webhookMap) throws Exception { return SHOPIFY_ALIGNED_MAPPER.writeValueAsBytes(webhookMap); } }
- 参考最优实现(从入口层缓存原始流,彻底避免还原问题,SpringBoot场景示例):
@PostMapping("/shopify/webhook") public ResponseEntity<?> handleWebhook(HttpServletRequest request) throws Exception { // 提前通过Request包装器缓存请求体字节,避免流被@RequestBody等逻辑消费后无法读取 byte[] rawRequestBytes = ((CachedRequestWrapper) request).getCachedBytes(); // 从请求头读取Shopify返回的签名 String signature = request.getHeader("X-Shopify-Hmac-Sha256"); // 直接用原始字节做HMAC校验,无需关心后续格式转换 if (!hmac256Verify(rawRequestBytes, signature, WEBHOOK_SECRET)) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } // 校验通过后再转Map/实体类做业务处理,转换逻辑不影响签名校验 Map<String, Object> webhookData = new ObjectMapper().readValue(rawRequestBytes, Map.class); processBusiness(webhookData); return ResponseEntity.ok().build(); }
2. Shopify Webhook的编码与签名规则
- 字符编码:所有Webhook请求体统一使用UTF-8无BOM编码,计算签名时直接取原始请求的字节数组,不会做任何编码转换。
- 签名算法:采用
HMAC-SHA256算法,签名密钥为Shopify后台Webhook配置页生成的专属Webhook Secret,和店铺API Access Token不是同一个值,注意不要混用。 - 签名生成逻辑:Shopify服务端生成待签名内容时,直接使用待发送的原始HTTP请求体字节流,不会对JSON做任何预处理(不会调整键顺序、不会增减空格、不会格式化输出、不会过滤字段),签名计算完成后对二进制结果做Base64编码,存入
X-Shopify-Hmac-Sha256请求头返回。 - 注意事项:请求体的任何改动(包括末尾多一个换行、字符串多一个引号、数字类型转字符串、键顺序调换)都会导致最终计算出的签名和Shopify返回的签名不一致。
内容的提问来源于stack exchange,提问作者Shikha
相关产品推荐
相关产品推荐

