refactor(openapi): 客户端签名统一为字典序+&拼接风格

- BuildOpenV1Sign 改为 app_key/body_sha256/method/nonce/path/timestamp
  按 ASCII 字典序用 & 拼接,与上游 BuildSignString 风格一致
- body 以 SHA256 摘要参与签名,避免大 body 与特殊字符问题
- 同步更新中间件调用点、测试用例与前端鉴权文档
This commit is contained in:
yml2213
2026-07-30 15:53:09 +08:00
parent 4165fc4d01
commit 881ef40fb7
3 changed files with 33 additions and 29 deletions
+15 -11
View File
@@ -35,7 +35,8 @@ type OpenAuthConfig struct {
} }
// OpenAuth 校验独立 API 客户端、时间戳、持久化 nonce 与 HMAC 签名。 // OpenAuth 校验独立 API 客户端、时间戳、持久化 nonce 与 HMAC 签名。
// 客户侧接口使用 X-App-Key 和 body SHA256;上游发货接口独立使用 SourceOpenAuth。 // 客户侧接口使用 X-App-Key + 字典序 & 拼接签名(app_key/body_sha256/method/nonce/path/timestamp);
// 上游发货接口独立使用 SourceOpenAuthapi_key + 原始 body)。
func OpenAuth(cfg OpenAuthConfig) gin.HandlerFunc { func OpenAuth(cfg OpenAuthConfig) gin.HandlerFunc {
if cfg.SkewSeconds <= 0 { if cfg.SkewSeconds <= 0 {
cfg.SkewSeconds = 300 cfg.SkewSeconds = 300
@@ -100,7 +101,7 @@ func OpenAuth(cfg OpenAuthConfig) gin.HandlerFunc {
method := strings.ToUpper(c.Request.Method) method := strings.ToUpper(c.Request.Method)
path := c.Request.URL.Path path := c.Request.URL.Path
expected := BuildOpenV1Sign(secret, timestamp, nonce, method, path, bodyBytes) expected := BuildOpenV1Sign(secret, appKey, timestamp, nonce, method, path, bodyBytes)
if !hmac.Equal([]byte(strings.ToLower(sign)), []byte(expected)) { if !hmac.Equal([]byte(strings.ToLower(sign)), []byte(expected)) {
response.Unauthorized(c, "签名校验失败") response.Unauthorized(c, "签名校验失败")
c.Abort() c.Abort()
@@ -164,20 +165,23 @@ func GetMerchantID(c *gin.Context) uint {
return merchantID return merchantID
} }
// BuildOpenV1Sign 生成开放接口签名:timestamp、nonce、method、path 与 body SHA256。 // BuildOpenV1Sign 生成客户侧开放接口签名:参数按 ASCII 字典序 + "&" 拼接,
func BuildOpenV1Sign(secret, timestamp, nonce, method, path string, body []byte) string { // 与上游 BuildSignString 风格一致;body 以 SHA256 摘要参与签名(避免大 body 与特殊字符问题)。
// 参与签名的参数固定顺序为:app_key, body_sha256, method, nonce, path, timestamp。
func BuildOpenV1Sign(secret, appKey, timestamp, nonce, method, path string, body []byte) string {
bodyHash := sha256.Sum256(body) bodyHash := sha256.Sum256(body)
content := strings.Join([]string{ content := strings.Join([]string{
timestamp, "app_key=" + appKey,
nonce, "body_sha256=" + hex.EncodeToString(bodyHash[:]),
strings.ToUpper(method), "method=" + strings.ToUpper(method),
path, "nonce=" + nonce,
hex.EncodeToString(bodyHash[:]), "path=" + path,
}, "\n") "timestamp=" + timestamp,
}, "&")
return hmacSHA256Hex(secret, content) return hmacSHA256Hex(secret, content)
} }
// BuildSignString 保留旧接口的字典序签名算法,供兼容客户端和测试使用。 // BuildSignString 保留旧接口的字典序签名算法,供上游发货兼容客户端和测试使用。
func BuildSignString(apiKey, timestamp, nonce, method, path, body string) string { func BuildSignString(apiKey, timestamp, nonce, method, path, body string) string {
return strings.Join([]string{ return strings.Join([]string{
"api_key=" + apiKey, "api_key=" + apiKey,
@@ -66,7 +66,7 @@ func TestOpenAuthV1AcceptsSignedRequestAndRejectsReplay(t *testing.T) {
ts := strconv.FormatInt(time.Now().Unix(), 10) ts := strconv.FormatInt(time.Now().Unix(), 10)
nonce := "nonce-123456" nonce := "nonce-123456"
path := "/api/client/v1/orders" path := "/api/client/v1/orders"
sign := BuildOpenV1Sign(secret, ts, nonce, http.MethodPost, path, []byte(body)) sign := BuildOpenV1Sign(secret, appKey, ts, nonce, http.MethodPost, path, []byte(body))
req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(body)) req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(body))
req.Header.Set("X-App-Key", appKey) req.Header.Set("X-App-Key", appKey)
+17 -17
View File
@@ -125,33 +125,32 @@ function AuthTab() {
<Card size="small" title="签名算法"> <Card size="small" title="签名算法">
<Descriptions size="small" column={1} bordered> <Descriptions size="small" column={1} bordered>
<Descriptions.Item label="签名内容"> <Descriptions.Item label="参与参数">
<Text code>{'timestamp\nnonce\nMETHOD\npath\nsha256(body)'}</Text> <Text code>app_key</Text><Text code>body_sha256</Text><Text code>method</Text><Text code>nonce</Text><Text code>path</Text><Text code>timestamp</Text>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="拼接方式"> <Text code>\n</Text> </Descriptions.Item> <Descriptions.Item label="拼接方式"> ASCII <Text code>&</Text> <Text code>k1=v1&k2=v2&...</Text></Descriptions.Item>
<Descriptions.Item label="METHOD"> GET / POST</Descriptions.Item> <Descriptions.Item label="排序后顺序">
<Text code>app_key, body_sha256, method, nonce, path, timestamp</Text>
</Descriptions.Item>
<Descriptions.Item label="method"> GET / POST</Descriptions.Item>
<Descriptions.Item label="path"> URL.Path query</Descriptions.Item> <Descriptions.Item label="path"> URL.Path query</Descriptions.Item>
<Descriptions.Item label="body">GET POST body </Descriptions.Item> <Descriptions.Item label="body_sha256">
<Text code>SHA256( body )</Text> GET body
</Descriptions.Item>
<Descriptions.Item label="value"> URL encode</Descriptions.Item>
<Descriptions.Item label="X-Sign"> <Descriptions.Item label="X-Sign">
<Text code>hex( HMAC-SHA256( app_secret, ) )</Text> <Text code>hex( HMAC-SHA256( app_secret, ) )</Text>
</Descriptions.Item> </Descriptions.Item>
</Descriptions> </Descriptions>
</Card> </Card>
<Card size="small" title="签名示例(GETbody 为空)"> <Card size="small" title="签名示例(GETbody 为空)">
<pre style={preStyle}>{`timestamp <pre style={preStyle}>{`app_key=ak_xxx&body_sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855&method=GET&nonce=a1b2c3d4e5f67890&path=/api/client/v1/products&timestamp=1721450000`}</pre>
nonce
GET
/api/client/v1/products
sha256("") = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`}</pre>
</Card> </Card>
<Card size="small" title="签名示例(POSTbody 非空)"> <Card size="small" title="签名示例(POSTbody 非空)">
<pre style={preStyle}>{`timestamp <pre style={preStyle}>{`# body = {"client_order_no":"shop-10001","sku":"suit_pink_sheep"}
nonce app_key=ak_xxx&body_sha256=<实际请求 body 字节的 SHA256 十六进制>&method=POST&nonce=a1b2c3d4e5f67890&path=/api/client/v1/orders&timestamp=1721450000`}</pre>
POST
/api/client/v1/orders
sha256(body) = <实际请求 body 字节的 SHA256 十六进制>`}</pre>
</Card> </Card>
<Alert <Alert
@@ -161,7 +160,8 @@ sha256(body) = <实际请求 body 字节的 SHA256 十六进制>`}</pre>
description={ description={
<ul style={{ margin: 0, paddingLeft: 20 }}> <ul style={{ margin: 0, paddingLeft: 20 }}>
<li>POST <Text code>body</Text> body <b></b></li> <li>POST <Text code>body</Text> body <b></b></li>
<li>secret path querybody nonce </li> <li>value <b></b> URL encode</li>
<li>secret path querybody nonce </li>
<li> <Text code>Idempotency-Key</Text> <Text code>client_order_no</Text> </li> <li> <Text code>Idempotency-Key</Text> <Text code>client_order_no</Text> </li>
</ul> </ul>
} }