概述
所有需要鉴权的 1Pass 接口(POST /token、POST /pay/wechat/native)都使用相同的 HMAC-SHA256 签名机制。理解签名流程后,你可以用任何语言完成接入。
第一步:构建规范字符串(Canonical String)
将以下 5 个部分用换行符 \n 拼接:
{timestamp}\n{nonce}\n{HTTP方法}\n{路径}\n{请求体的SHA256哈希}
| 部分 | 说明 | 示例 |
|---|---|---|
| timestamp | X-1Pass-Ts 的值(Unix 秒) |
1717590000 |
| nonce | X-1Pass-Nonce 的值 |
550e8400-e29b-41d4 |
| HTTP 方法 | 大写 | POST |
| 路径 | URL 路径(不含域名和查询字符串) | /token |
| 请求体哈希 | 请求体的 SHA-256 哈希(小写十六进制) | d7a8fbb3... |
空请求体:如果请求没有 body,对空字符串做 SHA-256:
SHA-256("") = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
第二步:计算签名
signature = Base64Url( HMAC-SHA256( SK, canonical_string ) )
- 密钥:使用创建/轮换时获得的原始 SK 字符串(如
sk_8YaAQS...) - 算法:HMAC-SHA256
- 编码:Base64Url —
+→-,/→_,去掉末尾=
❌ 不要对 SK 做哈希!直接使用原始 SK 字符串作为 HMAC 密钥。不要用 SHA-256(sk) 的结果。
完整签名示例
# 假设参数:
AK = "ak_3usbezGvCbsQuFmwyLn7VIjq7lguiRSK"
SK = "sk_8YaAQSdK_RyMJZqXzOAiCWmCEMvnv9Wf..."
body = '{"ticket":"TK_abc123"}'
ts = "1717590000"
nonce = "550e8400-e29b-41d4-a716-446655440000"
# 1. 计算请求体哈希
body_hash = SHA256('{"ticket":"TK_abc123"}')
# 2. 拼接规范字符串(5 行,用 \n 连接)
canonical = "1717590000\n550e8400-e29b-41d4-a716-446655440000\nPOST\n/token\n{body_hash}"
# 3. 计算签名
signature = Base64Url(HMAC-SHA256(SK, canonical))
# 4. 设置请求头
X-1Pass-AK: ak_3usbezGvCbsQuFmwyLn7VIjq7lguiRSK
X-1Pass-Ts: 1717590000
X-1Pass-Nonce: 550e8400-e29b-41d4-a716-446655440000
X-1Pass-Sign: {signature}
多语言签名示例
以下是各语言的完整 POST /token 调用示例,包含签名计算:
const crypto = require('crypto');
const AK = 'ak_xxxxxxxx'; // 替换为你的 AK
const SK = 'sk_xxxxxxxx'; // 替换为你的 SK
function sha256Hex(message) {
return crypto.createHash('sha256')
.update(message, 'utf8').digest('hex');
}
function hmacSha256Base64Url(key, message) {
const hmac = crypto.createHmac('sha256', key)
.update(message, 'utf8').digest('base64');
return hmac.replace(/\+/g, '-')
.replace(/\//g, '_').replace(/=+$/, '');
}
async function exchangeTicket(ticket) {
const ts = Math.floor(Date.now() / 1000).toString();
const nonce = crypto.randomUUID();
const body = JSON.stringify({ ticket });
const bodyHash = sha256Hex(body);
// 构建规范字符串(5 行,\n 连接)
const canonical =
`${ts}\n${nonce}\nPOST\n/token\n${bodyHash}`;
// 用原始 SK 签名
const signature = hmacSha256Base64Url(SK, canonical);
const resp = await fetch('https://1pass.top/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-1Pass-AK': AK,
'X-1Pass-Ts': ts,
'X-1Pass-Nonce': nonce,
'X-1Pass-Sign': signature,
},
body,
});
return await resp.json();
}
import hashlib, hmac, base64, json, time, uuid
import requests
AK = 'ak_xxxxxxxx' # 替换为你的 AK
SK = 'sk_xxxxxxxx' # 替换为你的 SK
def sha256_hex(message):
return hashlib.sha256(
message.encode('utf-8')).hexdigest()
def hmac_sha256_base64url(key, message):
sig = hmac.new(
key.encode('utf-8'),
message.encode('utf-8'),
hashlib.sha256).digest()
return base64.urlsafe_b64encode(sig) \
.rstrip(b'=').decode('ascii')
def exchange_ticket(ticket):
ts = str(int(time.time()))
nonce = str(uuid.uuid4())
body = json.dumps({"ticket": ticket})
body_hash = sha256_hex(body)
canonical = f"{ts}\n{nonce}\nPOST" \
f"\n/token\n{body_hash}"
signature = hmac_sha256_base64url(SK, canonical)
resp = requests.post(
"https://1pass.top/token",
headers={
"Content-Type": "application/json",
"X-1Pass-AK": AK,
"X-1Pass-Ts": ts,
"X-1Pass-Nonce": nonce,
"X-1Pass-Sign": signature,
},
data=body,
)
return resp.json()
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/google/uuid"
)
var (
AK = "ak_xxxxxxxx"
SK = "sk_xxxxxxxx"
)
func sha256Hex(msg string) string {
h := sha256.Sum256([]byte(msg))
return fmt.Sprintf("%x", h)
}
func sign(key, msg string) string {
mac := hmac.New(sha256.New, []byte(key))
mac.Write([]byte(msg))
return base64.RawURLEncoding.
EncodeToString(mac.Sum(nil))
}
func exchangeTicket(ticket string) (map[string]any, error) {
ts := fmt.Sprintf("%d", time.Now().Unix())
nonce := uuid.New().String()
body, _ := json.Marshal(map[string]string{
"ticket": ticket,
})
bodyHash := sha256Hex(string(body))
canonical := fmt.Sprintf(
"%s\n%s\nPOST\n/token\n%s",
ts, nonce, bodyHash)
signature := sign(SK, canonical)
req, _ := http.NewRequest("POST",
"https://1pass.top/token",
strings.NewReader(string(body)))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-1Pass-AK", AK)
req.Header.Set("X-1Pass-Ts", ts)
req.Header.Set("X-1Pass-Nonce", nonce)
req.Header.Set("X-1Pass-Sign", signature)
resp, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
var result map[string]any
json.Unmarshal(b, &result)
return result, nil
}
<?php
$AK = 'ak_xxxxxxxx';
$SK = 'sk_xxxxxxxx';
function sha256Hex($msg) {
return hash('sha256', $msg);
}
function hmacSign($key, $msg) {
$sig = hash_hmac('sha256', $msg, $key, true);
return rtrim(strtr(
base64_encode($sig), '+/', '-_'), '=');
}
function exchangeTicket($ticket) {
global $AK, $SK;
$ts = (string) time();
$nonce = sprintf('%s-%s-%s-%s-%s',
bin2hex(random_bytes(4)),
bin2hex(random_bytes(2)),
bin2hex(random_bytes(2)),
bin2hex(random_bytes(2)),
bin2hex(random_bytes(6)));
$body = json_encode(['ticket' => $ticket]);
$bodyHash = sha256Hex($body);
$canonical =
"{$ts}\n{$nonce}\nPOST\n/token\n{$bodyHash}";
$signature = hmacSign($SK, $canonical);
$ch = curl_init("https://1pass.top/token");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
"X-1Pass-AK: {$AK}",
"X-1Pass-Ts: {$ts}",
"X-1Pass-Nonce: {$nonce}",
"X-1Pass-Sign: {$signature}",
],
]);
$resp = curl_exec($ch);
curl_close($ch);
return json_decode($resp, true);
}
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.http.*;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;
import java.util.UUID;
public class OnePassClient {
static final String AK = "ak_xxxxxxxx";
static final String SK = "sk_xxxxxxxx";
static String sha256Hex(String msg) throws Exception {
byte[] h = MessageDigest.getInstance("SHA-256")
.digest(msg.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
for (byte b : h)
sb.append(String.format("%02x", b));
return sb.toString();
}
static String sign(String key, String msg)
throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(
key.getBytes(StandardCharsets.UTF_8),
"HmacSHA256"));
byte[] sig = mac.doFinal(
msg.getBytes(StandardCharsets.UTF_8));
return Base64.getUrlEncoder()
.withoutPadding().encodeToString(sig);
}
public static String exchangeTicket(String ticket)
throws Exception {
String ts = String.valueOf(
System.currentTimeMillis() / 1000);
String nonce = UUID.randomUUID().toString();
String body = "{\"ticket\":\"" + ticket + "\"}";
String bodyHash = sha256Hex(body);
String canonical = ts + "\n" + nonce
+ "\nPOST\n/token\n" + bodyHash;
String signature = sign(SK, canonical);
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://1pass.top/token"))
.header("Content-Type", "application/json")
.header("X-1Pass-AK", AK)
.header("X-1Pass-Ts", ts)
.header("X-1Pass-Nonce", nonce)
.header("X-1Pass-Sign", signature)
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
return HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString())
.body();
}
}
#!/bin/bash
AK="ak_xxxxxxxx"
SK="sk_xxxxxxxx"
TICKET="TK_xxxxx"
TS=$(date +%s)
NONCE=$(uuidgen | tr '[:upper:]' '[:lower:]')
BODY="{\"ticket\":\"${TICKET}\"}"
BODY_HASH=$(echo -n "$BODY" | shasum -a 256 | cut -d' ' -f1)
CANONICAL="${TS}\n${NONCE}\nPOST\n/token\n${BODY_HASH}"
SIGNATURE=$(echo -ne "$CANONICAL" \
| openssl dgst -sha256 -hmac "$SK" -binary \
| base64 \
| tr '+/' '-_' \
| tr -d '=')
curl -X POST "https://1pass.top/token" \
-H "Content-Type: application/json" \
-H "X-1Pass-AK: ${AK}" \
-H "X-1Pass-Ts: ${TS}" \
-H "X-1Pass-Nonce: ${NONCE}" \
-H "X-1Pass-Sign: ${SIGNATURE}" \
-d "$BODY"