鉴权体系说明
文档说明
本规范为全接口统一鉴权标准,平台所有对外提供的业务接口均采用 AKSK Bearer 鉴权方案。
重要约定
- 对接方要求:仅支持第三方后端服务(Node.js/Java/Python/Go 等服务端调用)
- 安全要求:禁止前端浏览器直接存储 SK,密钥必须存放于服务端环境变量或配置文件
- 鉴权实现:鉴权逻辑完全基于服务端源码实现,签名、校验规则全局统一
一、基础通用规范
接口通用基础规则
- 接口调用Base URL:
https://neolink.com/backend/
| 规则项 | 说明 |
|---|---|
| 鉴权覆盖 | 所有接口统一鉴权,无免登白名单接口 |
| HTTP 方法 | 支持 GET/POST/PUT/DELETE 任意 HTTP Method |
| Content-Type | 统一为 application/json |
| 鉴权位置 | 鉴权信息固定放在 HTTP 请求头 Authorization |
AK/SK 凭证说明
| 字段 | 类型 | 用途 | 安全要求 |
|---|---|---|---|
| AK (Access Key) | 公钥标识 | 标识调用方用户身份,参与签名计算 | 可随请求链路打印日志 |
| SK (Secret Key) | 私钥密钥 | HMAC-SHA256 签名密钥,校验核心凭证 | 严禁明文传输、前端存储、提交代码仓库,仅后端配置/环境变量读取 |
测试示例凭证
⚠️ 仅供参考,不可直接使用,AKSK在秘钥管理创建获取
AK: rak_if7r3r5rr2t1inhutldf
SK: rsk_tw4tfhidw9tuqu6r4vbgklmprp0xwrhr二、鉴权核心:Token 生成规范
Payload 结构体定义
Token 生成的签名载体结构(字段大小写严格区分):
json
{
"Login": "user-test",
"Reseller": "re-test",
"Ts": 1782965433,
"Nonce": "Xs92dF7s3kGd29sL"
}Payload 字段详细说明
| 字段 | 类型 | 必填 | 约束规则 |
|---|---|---|---|
| Login | string | 是 | 待操作的用户账号/手机号,该用户必须归属当前 AK 对应的用户 |
| Reseller | string | 否 | 代理商编码,与 AK 归属代理商一致;无代理商的直客业务传空字符串 "" |
| Ts | int64 | 是 | Unix 秒级时间戳;服务端校验客户端时间与服务端时间差值 ≤300s,超出直接拒绝 |
| Nonce | string | 是 | 密码学安全随机串,长度 8~128 位,仅大小写字母 + 数字;每次接口请求必须生成全新值,重复 Nonce 判定为重放攻击拦截 |
Token 完整生成步骤(顺序不可颠倒)
步骤 1:生成请求临时参数
- 生成 16 位随机 Nonce(推荐长度)
- 获取当前系统时间,转换为秒级时间戳
- JavaScript/TypeScript:
Math.floor(Date.now() / 1000) - 其他语言:使用对应的系统时间戳方法
- JavaScript/TypeScript:
步骤 2:序列化 Payload
将 Payload 对象转为标准 JSON 字符串,无多余空格、换行:
{"Login":"user-test","Reseller":"re-test","Ts":1782965433,"Nonce":"Xs92dF7s3kGd29sL"}步骤 3:URL-Safe Raw Base64 编码
等价 Go 标准库 base64.RawURLEncoding,转换规则:
- 执行标准 Base64 编码
- 字符替换:
+→-,/→_ - 删除末尾所有填充符号
=
示例输出:
eyJMb2dpbiI6InVzZXItdGVzdCIsIlJlc2VsbGVyIjoicmUtdGVzdCIsIlRzIjoxNzgyOTY1NDMzLCJOb25jZSI6IlhzOTJkRjdzM2tHZDI5c0wifQ步骤 4:拼接签名原文
signSource = {AK}.{PayloadBase64字符串}
示例:
signSource = rak_if7r3r5rr2t1inhutldf.eyJMb2dpbiI6InVzZXItdGVzdCIsIlJlc2VsbGVyIjoicmUtdGVzdCIsIlRzIjoxNzgyOTY1NDMzLCJOb25jZSI6IlhzOTJkRjdzM2tHZDI5c0wifQ步骤 5:HMAC-SHA256 计算签名
- 密钥:SK 明文
- 哈希算法:SHA256
- 输出格式:小写十六进制字符串(大写会验签失败)
步骤 6:拼接完整 AK-SK Token
token = aksk_{AK}.{PayloadBase64}.{HMAC签名Hex}
示例:
token = aksk_rak_if7r3r5rr2t1inhutldf.eyJMb2dpbiI6InVzZXItdGVzdCIsIlJlc2VsbGVyIjoicmUtdGVzdCIsIlRzIjoxNzgyOTY1NDMzLCJOb25jZSI6IlhzOTJkRjdzM2tHZDI5c0wifQ.a7f29d0c8e1b356xxxx💡 前缀
aksk_固定不可修改
步骤 7:组装鉴权请求头
Authorization: Bearer {token}三、通用请求头规范
所有接口统一遵循以下请求头规范:
| Header Key | 是否必填 | 说明 |
|---|---|---|
| Authorization | 是 | Bearer + 上文生成的完整 aksk token |
| Content-Type | 是 | 固定 application/json |
| X-Accept-Language | 可选 | 传 zh 时,业务错误 message 返回中文文案 |
| Referer | 可选 | 代理商前端域名,用于平台统计代理商来源;无则不传(代理商用户必传,个人用户非必传) |
完整请求头示例
http
Authorization: Bearer aksk_rak_if7r3r5rr2t1inhutldf.eyJMb2dpbiI6Imd4eS10ZXN0IiwiUmVzZWxsZXIiOiJjbXE6IiwiVHM6IjE3ODI5NjU0MzMiLCJOb25jZSI6IlhzOTJkRjdzM2tHZDI5c0wifQ.a7f29d0c8e1b356xxxx
Content-Type: application/json
X-Accept-Language: zh
Referer: https://re-test.example.com/四、通用返回格式
所有接口 HTTP 状态码统一返回 200,业务成功/失败通过 JSON 内 success 字段区分:
json
{
"success": true,
"message": "操作描述文案",
"data": {}
}鉴权相关标准错误返回
AK/SK 认证阶段报错(HTTP 401)
| 错误信息 | 触发条件 | 阶段 |
|---|---|---|
| Invalid AK/SK token format | Token 格式不是 aksk_<ak>.<payload>.<sig> 三段 | Token 解析 |
| Signature verification failed | AK 不存在或签名验证失败 | 凭据查询 / 签名验证 |
| Invalid or expired timestamp | 时间戳不在允许窗口(默认 ±300 秒)内 | 时间戳校验 |
| Replay request detected | 同一 AK+Nonce 在 TTL 内重复提交 | 防重放检查 |
| User not found | payload.login 匹配的用户不存在 | 用户查询 |
五、对接常见坑点 ⚠️
1. 时间戳单位错误
问题:JavaScript/TypeScript 原生 Date.now() 是毫秒
解决:必须除以 1000 向下取整转为秒,否则直接判定时间戳非法
javascript
// ❌ 错误
const ts = Date.now();
// ✅ 正确
const ts = Math.floor(Date.now() / 1000);2. Base64 格式不标准
问题:未替换 +/-、未删除末尾 = 填充符
解决:严格按照 URL-Safe Raw Base64 编码规则处理
3. Payload 字段大小写错误
问题:使用小写 login/reseller/ts/nonce 会导致服务端 JSON 解析错乱
解决:必须使用大小写字段名:Login/Reseller/Ts/Nonce
4. 签名输出大写 Hex
问题:HMAC 摘要输出为大写字符串
解决:HMAC 摘要必须转小写,大写字符串无法通过服务端校验
python
# ❌ 错误
signature = hmac.new(sk.encode(), sign_source.encode(), hashlib.sha256).hexdigest().upper()
# ✅ 正确
signature = hmac.new(sk.encode(), sign_source.encode(), hashlib.sha256).hexdigest()5. Nonce 复用
问题:同一随机串短时间内重复请求,触发重放拦截
解决:每次请求必须全新生成 Nonce
6. SK 明文泄露风险
问题:SK 被打印日志、前端存储、提交代码仓库
解决:SK 仅允许存放于后端服务环境变量或加密配置
六、代码示例
python
import hmac
import hashlib
import base64
import json
import time
import random
import string
import requests
from urllib.parse import quote
class AKSKAuth:
def __init__(self, ak, sk):
self.ak = ak
self.sk = sk
def generate_nonce(self, length=16):
"""生成指定长度的随机 Nonce"""
chars = string.ascii_letters + string.digits
return ''.join(random.choice(chars) for _ in range(length))
def get_timestamp(self):
"""获取当前秒级时间戳"""
return int(time.time())
def build_payload(self, login, reseller=""):
"""构建 Payload 对象"""
return {
"Login": login,
"Reseller": reseller,
"Ts": self.get_timestamp(),
"Nonce": self.generate_nonce()
}
def encode_payload(self, payload):
"""将 Payload 转为 URL-Safe Raw Base64 编码"""
json_str = json.dumps(payload, separators=(',', ':'), ensure_ascii=False)
base64_encoded = base64.b64encode(json_str.encode()).decode()
url_safe = base64_encoded.replace('+', '-').replace('/', '_').rstrip('=')
return url_safe
def generate_signature(self, ak, payload_base64):
"""生成 HMAC-SHA256 签名"""
sign_source = f"{ak}.{payload_base64}"
signature = hmac.new(
self.sk.encode(),
sign_source.encode(),
hashlib.sha256
).hexdigest()
return signature, sign_source
def generate_token(self, login, reseller=""):
"""生成完整的 AK/SK Token"""
payload = self.build_payload(login, reseller)
payload_base64 = self.encode_payload(payload)
signature, _ = self.generate_signature(self.ak, payload_base64)
token = f"aksk_{self.ak}.{payload_base64}.{signature}"
return token
def get_auth_header(self, login, reseller=""):
"""获取完整的 Authorization 请求头"""
token = self.generate_token(login, reseller)
return f"Bearer {token}"
def request_with_auth(self, method, url, login, reseller="", **kwargs):
"""使用 AK/SK 认证发送 HTTP 请求"""
headers = kwargs.get('headers', {})
headers['Authorization'] = self.get_auth_header(login, reseller)
headers['Content-Type'] = 'application/json'
kwargs['headers'] = headers
response = requests.request(method, url, **kwargs)
return response
# 使用示例
if __name__ == "__main__":
AK = "rak_if7r3r5rr2t1inhutldf"
SK = "rsk_tw4tfhidw9tuqu6r4vbgklmprp0xwrhr"
auth = AKSKAuth(AK, SK)
token = auth.generate_token(login="user-test", reseller="re-test")
print(f"Generated Token: {token}")
auth_header = auth.get_auth_header(login="user-test", reseller="re-test")
print(f"Authorization: {auth_header}")typescript
import axios, { AxiosRequestConfig } from 'axios';
import crypto from 'crypto';
interface PayloadData {
Login: string;
Reseller: string;
Ts: number;
Nonce: string;
}
class AKSKAuth {
private ak: string;
private sk: string;
constructor(ak: string, sk: string) {
this.ak = ak;
this.sk = sk;
}
private generateNonce(length: number = 16): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
private getTimestamp(): number {
return Math.floor(Date.now() / 1000);
}
private buildPayload(login: string, reseller: string = ''): PayloadData {
return {
Login: login,
Reseller: reseller,
Ts: this.getTimestamp(),
Nonce: this.generateNonce()
};
}
private encodePayload(payload: PayloadData): string {
const jsonStr = JSON.stringify(payload);
const base64Encoded = Buffer.from(jsonStr).toString('base64');
const urlSafe = base64Encoded
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
return urlSafe;
}
private generateSignature(ak: string, payloadBase64: string): { signature: string; signSource: string } {
const signSource = `${ak}.${payloadBase64}`;
const signature = crypto
.createHmac('sha256', this.sk)
.update(signSource)
.digest('hex');
return { signature, signSource };
}
public generateToken(login: string, reseller: string = ''): string {
const payload = this.buildPayload(login, reseller);
const payloadBase64 = this.encodePayload(payload);
const { signature } = this.generateSignature(this.ak, payloadBase64);
const token = `aksk_${this.ak}.${payloadBase64}.${signature}`;
return token;
}
public getAuthHeader(login: string, reseller: string = ''): string {
const token = this.generateToken(login, reseller);
return `Bearer ${token}`;
}
public async requestWithAuth(
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
url: string,
login: string,
reseller: string = '',
data?: any,
headers?: any
) {
const config: AxiosRequestConfig = {
method,
url,
headers: {
...headers,
'Authorization': this.getAuthHeader(login, reseller),
'Content-Type': 'application/json'
}
};
if (data) {
config.data = data;
}
return axios(config);
}
}
// 使用示例
(async () => {
const AK = 'rak_if7r3r5rr2t1inhutldf';
const SK = 'rsk_tw4tfhidw9tuqu6r4vbgklmprp0xwrhr';
const auth = new AKSKAuth(AK, SK);
const token = auth.generateToken('user-test', 're-test');
console.log(`Generated Token: ${token}`);
const authHeader = auth.getAuthHeader('user-test', 're-test');
console.log(`Authorization: ${authHeader}`);
})();go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"math/rand"
"net/http"
"strings"
"time"
"github.com/go-resty/resty/v2"
)
type PayloadData struct {
Login string `json:"Login"`
Reseller string `json:"Reseller"`
Ts int64 `json:"Ts"`
Nonce string `json:"Nonce"`
}
type AKSKAuth struct {
AK string
SK string
}
func (a *AKSKAuth) GenerateNonce(length int) string {
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
b := make([]byte, length)
for i := range b {
b[i] = charset[rand.Intn(len(charset))]
}
return string(b)
}
func (a *AKSKAuth) GetTimestamp() int64 {
return time.Now().Unix()
}
func (a *AKSKAuth) BuildPayload(login, reseller string) PayloadData {
return PayloadData{
Login: login,
Reseller: reseller,
Ts: a.GetTimestamp(),
Nonce: a.GenerateNonce(16),
}
}
func (a *AKSKAuth) EncodePayload(payload PayloadData) (string, error) {
jsonBytes, err := json.Marshal(payload)
if err != nil {
return "", err
}
urlSafe := base64.RawURLEncoding.EncodeToString(jsonBytes)
return urlSafe, nil
}
func (a *AKSKAuth) GenerateSignature(ak, payloadBase64 string) (string, string) {
signSource := fmt.Sprintf("%s.%s", ak, payloadBase64)
h := hmac.New(sha256.New, []byte(a.SK))
h.Write([]byte(signSource))
signature := fmt.Sprintf("%x", h.Sum(nil))
return signature, signSource
}
func (a *AKSKAuth) GenerateToken(login, reseller string) (string, error) {
payload := a.BuildPayload(login, reseller)
payloadBase64, err := a.EncodePayload(payload)
if err != nil {
return "", err
}
signature, _ := a.GenerateSignature(a.AK, payloadBase64)
token := fmt.Sprintf("aksk_%s.%s.%s", a.AK, payloadBase64, signature)
return token, nil
}
func (a *AKSKAuth) GetAuthHeader(login, reseller string) (string, error) {
token, err := a.GenerateToken(login, reseller)
if err != nil {
return "", err
}
return fmt.Sprintf("Bearer %s", token), nil
}
// 使用示例
func main() {
AK := "rak_if7r3r5rr2t1inhutldf"
SK := "rsk_tw4tfhidw9tuqu6r4vbgklmprp0xwrhr"
auth := &AKSKAuth{AK: AK, SK: SK}
token, err := auth.GenerateToken("user-test", "re-test")
if err != nil {
fmt.Printf("Error generating token: %v\n", err)
return
}
fmt.Printf("Generated Token: %s\n", token)
authHeader, err := auth.GetAuthHeader("user-test", "re-test")
if err != nil {
fmt.Printf("Error getting auth header: %v\n", err)
return
}
fmt.Printf("Authorization: %s\n", authHeader)
}java
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import okhttp3.*;
import org.apache.commons.codec.binary.Hex;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Random;
public class AKSKAuth {
private String ak;
private String sk;
private Gson gson;
public AKSKAuth(String ak, String sk) {
this.ak = ak;
this.sk = sk;
this.gson = new Gson();
}
private String generateNonce(int length) {
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
StringBuilder sb = new StringBuilder();
Random random = new Random();
for (int i = 0; i < length; i++) {
sb.append(chars.charAt(random.nextInt(chars.length())));
}
return sb.toString();
}
private long getTimestamp() {
return System.currentTimeMillis() / 1000;
}
private JsonObject buildPayload(String login, String reseller) {
JsonObject payload = new JsonObject();
payload.addProperty("Login", login);
payload.addProperty("Reseller", reseller);
payload.addProperty("Ts", getTimestamp());
payload.addProperty("Nonce", generateNonce(16));
return payload;
}
private String encodePayload(JsonObject payload) {
String jsonStr = gson.toJson(payload);
String base64Encoded = Base64.getEncoder().encodeToString(jsonStr.getBytes(StandardCharsets.UTF_8));
return base64Encoded
.replace("+", "-")
.replace("/", "_")
.replaceAll("=+$", "");
}
private String generateSignature(String ak, String payloadBase64) throws Exception {
String signSource = ak + "." + payloadBase64;
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(sk.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
mac.init(secretKey);
byte[] signature = mac.doFinal(signSource.getBytes(StandardCharsets.UTF_8));
return Hex.encodeHexString(signature);
}
public String generateToken(String login, String reseller) throws Exception {
JsonObject payload = buildPayload(login, reseller);
String payloadBase64 = encodePayload(payload);
String signature = generateSignature(ak, payloadBase64);
return String.format("aksk_%s.%s.%s", ak, payloadBase64, signature);
}
public String getAuthHeader(String login, String reseller) throws Exception {
String token = generateToken(login, reseller);
return "Bearer " + token;
}
public static void main(String[] args) {
String AK = "rak_if7r3r5rr2t1inhutldf";
String SK = "rsk_tw4tfhidw9tuqu6r4vbgklmprp0xwrhr";
AKSKAuth auth = new AKSKAuth(AK, SK);
try {
String token = auth.generateToken("user-test", "re-test");
System.out.println("Generated Token: " + token);
String authHeader = auth.getAuthHeader("user-test", "re-test");
System.out.println("Authorization: " + authHeader);
} catch (Exception e) {
e.printStackTrace();
}
}
}bash
#!/bin/bash
AK="rak_if7r3r5rr2t1inhutldf"
SK="rsk_tw4tfhidw9tuqu6r4vbgklmprp0xwrhr"
LOGIN="user-test"
RESELLER="re-test"
# 生成时间戳(秒级)
TIMESTAMP=$(date +%s)
# 生成随机 Nonce(16 位)
NONCE=$(openssl rand -hex 8 | tr -d '\n')
# 构建 Payload JSON
PAYLOAD="{\"Login\":\"$LOGIN\",\"Reseller\":\"$RESELLER\",\"Ts\":$TIMESTAMP,\"Nonce\":\"$NONCE\"}"
# Base64 URL-Safe 编码 Payload
PAYLOAD_BASE64=$(echo -n "$PAYLOAD" | base64 | tr '+/' '-_' | tr -d '=')
# 拼接签名原文
SIGN_SOURCE="$AK.$PAYLOAD_BASE64"
# 计算 HMAC-SHA256 签名(小写 Hex)
SIGNATURE=$(echo -n "$SIGN_SOURCE" | openssl dgst -sha256 -hmac "$SK" -hex | cut -d' ' -f2)
# 组装完整 Token
TOKEN="aksk_$AK.$PAYLOAD_BASE64.$SIGNATURE"
# 构建 Authorization 请求头
AUTH_HEADER="Bearer $TOKEN"
echo "Token: $TOKEN"
echo "Authorization: $AUTH_HEADER"
# 发送请求示例
curl -X POST https://api.example.com/v1/chat/completions \
-H "Authorization: $AUTH_HEADER" \
-H "Content-Type: application/json" \
-H "X-Accept-Language: zh" \
-d '{
"model": "gpt-4",
"messages": [
{"role": "user", "content": "Hello"}
]
}'下一步
- 🏢 企业管理
- 🔑 API Key 查询
- 💰 余额账单
- ⚙️ 系统管理
