身份认证
HashNut 使用 HMAC-SHA256 签名来验证 API 请求。部分接口使用请求头签名,另一些则在请求体中使用 accessSign 字段。
HMAC-SHA256 签名流程
签名过程包含四个步骤:
第一步:生成请求元数据
为每个请求生成一个唯一字符串和一个 Unix 时间戳(毫秒)。
第二步:构建签名字符串
将三个部分无分隔符地拼接在一起:
signString = uuid + timestamp + requestBody其中 requestBody 是请求体的 JSON 字符串。
第三步:计算签名
使用您的 Secret Key 计算 HMAC-SHA256,然后对结果进行 Base64 编码:
signature = base64( hmac_sha256( secretKey, signString ) )第四步:设置请求头
在 HTTP 请求中包含以下请求头:
| 请求头 | 说明 |
|---|---|
hashnut-request-uuid | 第一步中生成的唯一字符串,每次请求必须不同(重复会被判定为重放并拒绝) |
hashnut-request-timestamp | 第一步中生成的 Unix 毫秒时间戳,与服务端时间偏差不能超过 ±5 分钟 |
hashnut-request-sign | Base64 编码的 HMAC-SHA256 签名 |
Content-Type | 必须为 application/json |
代码示例
java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.UUID;
public class HashNutSign {
public static String[] sign(String secretKey, String body) throws Exception {
String reqUUID = UUID.randomUUID().toString();
String timestamp = String.valueOf(System.currentTimeMillis()); // 毫秒
String signString = reqUUID + timestamp + body;
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secretKey.getBytes(), "HmacSHA256"));
String signature = Base64.getEncoder().encodeToString(
mac.doFinal(signString.getBytes()));
return new String[]{reqUUID, timestamp, signature};
}
}go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"fmt"
"time"
"github.com/google/uuid"
)
func sign(secretKey, body string) (reqUUID, timestamp, signature string) {
reqUUID = uuid.New().String()
timestamp = fmt.Sprintf("%d", time.Now().UnixMilli()) // 毫秒
signString := reqUUID + timestamp + body
mac := hmac.New(sha256.New, []byte(secretKey))
mac.Write([]byte(signString))
signature = base64.StdEncoding.EncodeToString(mac.Sum(nil))
return reqUUID, timestamp, signature
}js
const crypto = require('crypto');
const { v4: uuidv4 } = require('uuid');
function sign(secretKey, body) {
const reqUUID = uuidv4();
const timestamp = Date.now().toString(); // 毫秒
const signString = reqUUID + timestamp + body;
const signature = crypto
.createHmac('sha256', secretKey)
.update(signString)
.digest('base64');
return { reqUUID, timestamp, signature };
}python
import hmac
import hashlib
import base64
import uuid
import time
def sign(secret_key: str, body: str):
req_uuid = str(uuid.uuid4())
timestamp = str(int(time.time() * 1000)) # 毫秒
sign_string = req_uuid + timestamp + body
signature = base64.b64encode(
hmac.new(
secret_key.encode(),
sign_string.encode(),
hashlib.sha256
).digest()
).decode()
return req_uuid, timestamp, signatureAccess Sign(请求体签名)
查询订单和确认支付不走请求头签名, 而是把 accessSign 放在请求体里。它的计算方式和上面的请求头签名完全不同:
accessSign = HMACSHA256(secretKey, 把 payOrderId 和 merchantOrderId 按不区分大小写升序排序后直接拼接)结果是大写十六进制字符串(64 字符),不是 base64。
IMPORTANT
你不需要自己算它——创建订单的响应里就返回了 data.accessSign。 把它连同 payOrderId 一起落库,后续查询和确认原样回传即可。 因为它只跟这两个 ID 有关、与请求体其余内容无关,所以同一笔订单的 accessSign 是固定值。
接口认证方式
| 接口 | 认证方式 | 说明 |
|---|---|---|
POST /v4.0.0/api/orders/create | 请求头签名 | 创建订单 |
POST /v4.0.0/api/orders/cancel | 请求头签名 | 取消订单 |
POST /v4.0.0/api/orders/query | 请求体 accessSign | 查询订单 |
POST /v4.0.0/pay/orders/confirm | 请求体 accessSign | 确认支付 |
POST /v4.0.0/api/orders/supplements | 无需认证 | 查询补款记录,只校验 payOrderId 格式 |
POST /v4.0.0/api/orders/supplements/latest | 无需认证 | 同上,只返回最近一条 |
POST /v4.0.0/config/* | 无需认证 | 公共配置接口 |
WARNING
补款记录查询接口不做任何鉴权,拿到合法 payOrderId 就能读到数据。 不要把 payOrderId 暴露在你自己的公开页面或前端可见的位置。