232 lines
7.0 KiB
Markdown
232 lines
7.0 KiB
Markdown
鉴权
|
||
每个请求在 Header 携带:
|
||
|
||
Header 说明
|
||
X-App-Key App Key
|
||
X-Timestamp Unix 秒级时间戳(字符串),与服务器相差不超过 5 分钟
|
||
X-Sign 签名(见下)
|
||
#签名算法
|
||
code
|
||
待签名字符串 = app_key + timestamp + raw_json_body
|
||
sign = HMAC_SHA256(待签名字符串, app_secret) // 小写十六进制
|
||
要点 说明
|
||
raw_json_body 与 HTTP Body 完全一致 的原始字符串;签名后再原样发送
|
||
是否排序 不需要对 JSON 字段排序;勿解析后再 json_encode 一次
|
||
Header app_key、timestamp 只放在 Header,不参与 Body JSON
|
||
中文 PHP 使用 JSON_UNESCAPED_UNICODE;Python 使用 ensure_ascii=False
|
||
商户后台 开放 API → 调用调试 需先通过密码或动态口令验证,方可加载密钥并查看待签名字符串、签名结果与最终发包参数。
|
||
|
||
#PHP
|
||
php
|
||
<?php
|
||
|
||
$appKey = 'your_app_key';
|
||
$appSecret = 'your_app_secret';
|
||
$apiBase = 'http://skin-exchange.yiquyou.icu';
|
||
|
||
$payload = [
|
||
'platform_order_no' => 'YOUR-ORDER-001',
|
||
'product_code' => '10000001',
|
||
'platform_buy_num' => 1,
|
||
];
|
||
|
||
// 生成 body 后不要再改,签名与 curl 必须用同一字符串
|
||
$body = json_encode($payload, JSON_UNESCAPED_UNICODE);
|
||
$timestamp = (string) time();
|
||
$preSign = $appKey . $timestamp . $body;
|
||
$sign = hash_hmac('sha256', $preSign, $appSecret);
|
||
|
||
$ch = curl_init($apiBase . '/api/open/v1/orders/store');
|
||
curl_setopt_array($ch, [
|
||
CURLOPT_POST => true,
|
||
CURLOPT_POSTFIELDS => $body,
|
||
CURLOPT_HTTPHEADER => [
|
||
'Content-Type: application/json',
|
||
'X-App-Key: ' . $appKey,
|
||
'X-Timestamp: ' . $timestamp,
|
||
'X-Sign: ' . $sign,
|
||
],
|
||
CURLOPT_RETURNTRANSFER => true,
|
||
]);
|
||
$response = curl_exec($ch);
|
||
curl_close($ch);
|
||
#Java
|
||
依赖 JDK 标准库(javax.crypto),无需第三方包。
|
||
|
||
java
|
||
import javax.crypto.Mac;
|
||
import javax.crypto.spec.SecretKeySpec;
|
||
import java.net.URI;
|
||
import java.net.http.HttpClient;
|
||
import java.net.http.HttpRequest;
|
||
import java.net.http.HttpResponse;
|
||
import java.nio.charset.StandardCharsets;
|
||
import java.time.Instant;
|
||
|
||
public class OpenApiSignExample {
|
||
|
||
private static String hmacSha256Hex(String secret, String message) throws Exception {
|
||
Mac mac = Mac.getInstance("HmacSHA256");
|
||
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
|
||
byte[] hash = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
|
||
StringBuilder hex = new StringBuilder(hash.length * 2);
|
||
for (byte b : hash) {
|
||
hex.append(String.format("%02x", b));
|
||
}
|
||
return hex.toString();
|
||
}
|
||
|
||
public static void main(String[] args) throws Exception {
|
||
String appKey = "your_app_key";
|
||
String appSecret = "your_app_secret";
|
||
String apiBase = "http://skin-exchange.yiquyou.icu";
|
||
|
||
// 与最终请求体一致;可用 Gson/Jackson 生成,但不要签名后再格式化
|
||
String body = "{\"platform_order_no\":\"YOUR-ORDER-001\",\"product_code\":\"10000001\",\"platform_buy_num\":1}";
|
||
String timestamp = String.valueOf(Instant.now().getEpochSecond());
|
||
String preSign = appKey + timestamp + body;
|
||
String sign = hmacSha256Hex(appSecret, preSign);
|
||
|
||
HttpRequest request = HttpRequest.newBuilder()
|
||
.uri(URI.create(apiBase + "/api/open/v1/orders/store"))
|
||
.header("Content-Type", "application/json")
|
||
.header("X-App-Key", appKey)
|
||
.header("X-Timestamp", timestamp)
|
||
.header("X-Sign", sign)
|
||
.POST(HttpRequest.BodyPublishers.ofString(body))
|
||
.build();
|
||
|
||
HttpResponse<String> response = HttpClient.newHttpClient()
|
||
.send(request, HttpResponse.BodyHandlers.ofString());
|
||
System.out.println(response.statusCode());
|
||
System.out.println(response.body());
|
||
}
|
||
}
|
||
#Python
|
||
Python 3,hmac + hashlib 标准库。
|
||
|
||
python
|
||
import hashlib
|
||
import hmac
|
||
import json
|
||
import time
|
||
import urllib.request
|
||
|
||
APP_KEY = "your_app_key"
|
||
APP_SECRET = "your_app_secret"
|
||
API_BASE = "http://skin-exchange.yiquyou.icu"
|
||
|
||
payload = {
|
||
"platform_order_no": "YOUR-ORDER-001",
|
||
"product_code": "10000001",
|
||
"platform_buy_num": 1,
|
||
}
|
||
|
||
# separators 生成紧凑 JSON,与常见 PHP json_encode 一致;签名后勿再 dumps
|
||
body = json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
|
||
timestamp = str(int(time.time()))
|
||
pre_sign = APP_KEY + timestamp + body
|
||
sign = hmac.new(
|
||
APP_SECRET.encode("utf-8"),
|
||
pre_sign.encode("utf-8"),
|
||
hashlib.sha256,
|
||
).hexdigest()
|
||
|
||
req = urllib.request.Request(
|
||
API_BASE + "/api/open/v1/orders/store",
|
||
data=body.encode("utf-8"),
|
||
method="POST",
|
||
headers={
|
||
"Content-Type": "application/json",
|
||
"X-App-Key": APP_KEY,
|
||
"X-Timestamp": timestamp,
|
||
"X-Sign": sign,
|
||
},
|
||
)
|
||
with urllib.request.urlopen(req) as resp:
|
||
print(resp.status)
|
||
print(resp.read().decode())
|
||
使用 requests 时:
|
||
|
||
python
|
||
import requests
|
||
|
||
resp = requests.post(
|
||
API_BASE + "/api/open/v1/orders/store",
|
||
data=body.encode("utf-8"), # 传 bytes,避免库再次序列化
|
||
headers={
|
||
"Content-Type": "application/json",
|
||
"X-App-Key": APP_KEY,
|
||
"X-Timestamp": timestamp,
|
||
"X-Sign": sign,
|
||
},
|
||
timeout=30,
|
||
)
|
||
print(resp.status_code, resp.text)
|
||
#Go
|
||
go
|
||
package main
|
||
|
||
import (
|
||
"bytes"
|
||
"crypto/hmac"
|
||
"crypto/sha256"
|
||
"encoding/hex"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"time"
|
||
)
|
||
|
||
func sign(appKey, appSecret, body string) (timestamp, sign string) {
|
||
timestamp = fmt.Sprintf("%d", time.Now().Unix())
|
||
mac := hmac.New(sha256.New, []byte(appSecret))
|
||
mac.Write([]byte(appKey + timestamp + body))
|
||
sign = hex.EncodeToString(mac.Sum(nil))
|
||
return
|
||
}
|
||
|
||
func main() {
|
||
appKey := "your_app_key"
|
||
appSecret := "your_app_secret"
|
||
apiBase := "http://skin-exchange.yiquyou.icu"
|
||
|
||
// 建议用 struct + json.Marshal;保证签名用的 body 与 Post 发送的字节一致
|
||
type orderReq struct {
|
||
PlatformOrderNo string `json:"platform_order_no"`
|
||
ProductCode string `json:"product_code"`
|
||
PlatformBuyNum int `json:"platform_buy_num"`
|
||
}
|
||
bodyBytes, _ := json.Marshal(orderReq{
|
||
PlatformOrderNo: "YOUR-ORDER-001",
|
||
ProductCode: "10000001",
|
||
PlatformBuyNum: 1,
|
||
})
|
||
body := string(bodyBytes)
|
||
|
||
timestamp, sig := sign(appKey, appSecret, body)
|
||
|
||
req, _ := http.NewRequest(http.MethodPost, apiBase+"/api/open/v1/orders/store", bytes.NewReader(bodyBytes))
|
||
req.Header.Set("Content-Type", "application/json")
|
||
req.Header.Set("X-App-Key", appKey)
|
||
req.Header.Set("X-Timestamp", timestamp)
|
||
req.Header.Set("X-Sign", sig)
|
||
|
||
resp, err := http.DefaultClient.Do(req)
|
||
if err != nil {
|
||
panic(err)
|
||
}
|
||
defer resp.Body.Close()
|
||
out, _ := io.ReadAll(resp.Body)
|
||
fmt.Println(resp.Status, string(out))
|
||
}
|
||
#常见错误
|
||
现象 原因
|
||
签名校验失败 Body 与签名时不一致(多空格、字段顺序、Unicode 转义不同)
|
||
timestamp 无效或已过期 服务器时间差超过 5 分钟
|
||
缺少 app_key、timestamp 或 sign Header 名称或大小写错误(应为 X-App-Key 等)
|
||
排查:用商户后台 调用调试 对比「待签名字符串」与己方代码输出是否一致。
|
||
|
||
|