帮助中心 / 开发者文档 / 签名算法详解

概述

所有需要鉴权的 1Pass 接口(POST /tokenPOST /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"