跳到主要内容

HMAC 签名校验

Shoplazza 在几类场景使用 HMAC-SHA256 签名,让你的应用确认请求确实来自 Shoplazza、未被中间人篡改:

场景用途算法
OAuth 安装回调 / 授权回调验证 query 参数完整性HMAC-SHA256(query 排序拼接, client_secret)
Webhook 投递验证 payload 完整性HMAC-SHA256(原始 body, client_secret) → base64

本页定位:HMAC 算法的统一中文参考。

想跟着步骤把签名校验整合到代码里:去 用 Node.js 与 Express 构建应用

通用要素

  • 算法:HMAC-SHA256
  • 密钥:Client Secret(合作伙伴中心生成,仅服务端可见)
  • 编码:输出 小写 hex digest
  • 对比:使用 时间安全比较(如 Node 的 crypto.timingSafeEqual)避免时序攻击

场景 1:OAuth 安装 / 授权回调签名

算法步骤

  1. 从 query 参数中移除 hmac 字段
  2. 把剩余参数按 key 字母升序 排序
  3. key=value 拼接,分隔符为 &不要做 URL 编码
  4. client_secret 作为密钥,对拼接结果做 HMAC-SHA256,输出小写 hex
  5. 与请求中的 hmac 做时间安全比较

示例输入

请求:

GET /auth/install?hmac=c4caf9b0...&install_from=app_store&shop=xxx.myshoplaza.com&store_id=1339409

移除 hmac 并排序后的待签名串:

install_from=app_store&shop=xxx.myshoplaza.com&store_id=1339409

参考实现

import crypto from 'crypto';

function hmacValidator(req, res, next) {
const { hmac } = req.query;
if (!hmac) {
return res.status(400).json({ message: '缺少 hmac 参数' });
}

const map = { ...req.query };
delete map.hmac;
const sortedKeys = Object.keys(map).sort();
const message = sortedKeys.map(key => `${key}=${map[key]}`).join('&');

const generatedHash = crypto
.createHmac('sha256', process.env.CLIENT_SECRET)
.update(message)
.digest('hex');

if (!secureCompare(generatedHash, hmac)) {
return res.status(400).json({ message: 'HMAC 校验失败' });
}

next();
}

function secureCompare(a, b) {
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
}

也可使用 Shoplazza OAuth SDK(Go)快速校验 shop 与 hmac:

import (
co "github.com/shoplazza-os/oauth-sdk-go"
"github.com/shoplazza-os/oauth-sdk-go/shoplazza"
)

oauth := &co.Config{
ClientID: "s1Ip1WxpoEAHtPPzGiP2rK2Az-P07Nie7V97hRKigl4",
ClientSecret: "{YOUR_CLIENT_SECRET}",
Endpoint: shoplazza.Endpoint,
RedirectURI: "https://3830-43-230-206-233.ngrok.io/oauth_sdk/redirect_uri/",
Scopes: []string{"read_shop"},
}
oauth.ValidShop("xxx.myshoplaza.com") // verify shop parameter

var requestUrl = "http://example.com/some/redirect_uri?code={authorization_code}&shop={store_name}.myshoplaza.com&hmac={hmac}"
query := strings.Split(requestUrl, "?")
params, _ := url.ParseQuery(query[1])
oauth.SignatureValid(params) // verify hmac

常见踩坑

  • ❌ 排序后又重新做 URL 编码——会得到不同的哈希
  • ❌ 字符串比较用 ==——存在时序攻击风险
  • ❌ 把 hmac 留在参数里参与签名——会自指
  • ❌ 用 SHA-256 而非 HMAC-SHA256(少了密钥)

场景 2:Webhook payload 签名

Webhook 的验签与场景 1 不同:对请求的原始 body 做 HMAC-SHA256、输出 base64(场景 1 是对排序后的 query 串、输出 hex)。

每个 webhook 请求都带一个 base64 编码的 X-Shoplazza-Hmac-Sha256 请求头,由应用的 Client Secret 对请求体数据生成。验证时按下述算法计算 HMAC 摘要,与该 header 的值做时间安全比较,一致即说明请求来自 Shoplazza。最佳实践:在应用响应 webhook 之前完成验证。

算法步骤

  1. 取请求的原始 body 字节流(不要先解析成 JSON 再重新序列化)
  2. client_secret 作为密钥,对原始 body 做 HMAC-SHA256
  3. 把摘要做 base64 编码
  4. 与请求头 X-Shoplazza-Hmac-Sha256 的值做时间安全比较

参考实现

require 'rubygems'
require 'base64'
require 'openssl'
require 'sinatra'

# Shoplazza 的 Client Secret
SECRET = 'my_secret'

helpers do
# 用共享密钥对请求体计算 HMAC,与请求头中的 HMAC 比较
def verify_webhook(data, hmac_header)
calculated_hmac = Base64.strict_encode64(OpenSSL::HMAC.digest('sha256', SECRET, data))
ActiveSupport::SecurityUtils.secure_compare(calculated_hmac, hmac_header)
end
end

# 响应 HTTP POST 请求
post '/' do
request.body.rewind
data = request.body.read
verified = verify_webhook(data, env["X-Shoplazza-Hmac-Sha256"])
puts "Webhook verified: #{verified}"
end

与场景 1 的差异:场景 1 对排序后的 query 串签名、输出 hex;场景 2 对原始 body 签名、输出 base64,请求头为 X-Shoplazza-Hmac-Sha256

相关