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 安装 / 授权回调签名
算法步骤
- 从 query 参数中移除
hmac字段 - 把剩余参数按 key 字母升序 排序
- 用
key=value拼接,分隔符为&(不要做 URL 编码) - 用
client_secret作为密钥,对拼接结果做 HMAC-SHA256,输出小写 hex - 与请求中的
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
参考实现
- Node.js
- Python
- Go
- Ruby
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));
}
import hmac as hmac_lib
import hashlib
from urllib.parse import parse_qs
def verify_hmac(query_string: str, client_secret: str) -> bool:
params = {k: v[0] for k, v in parse_qs(query_string).items()}
received_hmac = params.pop('hmac', None)
if not received_hmac:
return False
message = '&'.join(f'{k}={params[k]}' for k in sorted(params.keys()))
generated = hmac_lib.new(
client_secret.encode('utf-8'),
message.encode('utf-8'),
hashlib.sha256,
).hexdigest()
return hmac_lib.compare_digest(generated, received_hmac)
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"net/url"
"sort"
"strings"
)
func VerifyHMAC(query url.Values, clientSecret string) bool {
received := query.Get("hmac")
if received == "" {
return false
}
query.Del("hmac")
keys := make([]string, 0, len(query))
for k := range query {
keys = append(keys, k)
}
sort.Strings(keys)
parts := make([]string, len(keys))
for i, k := range keys {
parts[i] = k + "=" + query.Get(k)
}
message := strings.Join(parts, "&")
mac := hmac.New(sha256.New, []byte(clientSecret))
mac.Write([]byte(message))
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(received))
}
def verified_hmac?(hmac)
sha256 = OpenSSL::Digest::SHA256.new
query_string = "code=1vtke5ljOOL2jPds6gM0TNCeYZDitYB&shop=simon.myshoplaza.com"
calculated_hmac = OpenSSL::HMAC.hexdigest(sha256, CLIENT_SECRET, query_string)
ActiveSupport::SecurityUtils.secure_compare(calculated_hmac, hmac)
end
也可使用 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 之前完成验证。
算法步骤
- 取请求的原始 body 字节流(不要先解析成 JSON 再重新序列化)
- 用
client_secret作为密钥,对原始 body 做 HMAC-SHA256 - 把摘要做 base64 编码
- 与请求头
X-Shoplazza-Hmac-Sha256的值做时间安全比较
参考实现
- Ruby (Sinatra)
- Node.js
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
import crypto from 'crypto';
// 必须拿到原始 body(如用 express.raw()),不能用解析后的 JSON 重新序列化
function verifyWebhook(rawBody, hmacHeader) {
const calculated = crypto
.createHmac('sha256', process.env.CLIENT_SECRET)
.update(rawBody)
.digest('base64');
return crypto.timingSafeEqual(Buffer.from(calculated), Buffer.from(hmacHeader));
}
与场景 1 的差异:场景 1 对排序后的 query 串签名、输出 hex;场景 2 对原始 body 签名、输出 base64,请求头为
X-Shoplazza-Hmac-Sha256。
相关
- OAuth 鉴权 — 场景 1 在 OAuth 流程中的位置
- Webhook 概览 — 场景 2 的上下文(Webhook 订阅与生命周期)
- 用 Node.js 与 Express 构建应用 — 完整 Node.js + Express 实战