4. 令牌生成接口规范
这一章我们来聊令牌生成接口。这是令牌化服务的核心入口,所有的数据保护都从这里开始。我在项目里见过不少设计,有的把生成参数堆得跟超市货架一样,有的又精简到连调用方该传什么都不清楚。
咱们今天就把请求格式、响应字段、参数说明与约束一次理清。你照着这个写,基本不会踩坑。
4.1 请求设计原则
我需要你记住三个字:少而精。
令牌生成接口不是万能工具箱。它只负责一件事:输入敏感数据,输出令牌和必要的元数据。别的逻辑别往里塞。
核心约束:
- 严禁把令牌映射表直接暴露给调用方
- 每次请求必须携带幂等键(Idempotency-Key),防止重复生成
- 请求体使用 JSON,字符编码统一 UTF-8
4.2 请求格式规范
我之前接手的某支付项目,客户端传了个 Base64 编码的 XML 报文进来,解析直接挂了。你说头疼不头疼。所以咱们强制统一。
| 元素 | 说明 | 约束 |
|---|---|---|
method |
请求方法 | 仅支持 POST |
path |
端点路径 | /api/v1/tokens |
content-type |
媒体类型 | application/json; charset=utf-8 |
idempotency-key |
幂等键(Header) | UUID v4,必填 |
request-id |
跟踪 ID(Header) | 必填,用于链路追踪 |
你可能会问,为什么幂等键不放在地址栏?嗯,我当时也这么想过,直到我深究了 HTTP 语义。幂等性和请求方法是绑定的,放在 Header 里才是正路。
请求体结构示例:
{
"plaintext": "622202******1234",
"context": {
"domain": "payment",
"user_id": "u_abc123",
"expiration": "2026-12-31T23:59:59Z"
},
"options": {
"_type": "PAN",
"lifetime_seconds": 3600,
"reversible": false
}
}
4.3 参数说明与约束
每个字段都有它的脾气。下面这张表我建议你存一份,联调时对着看。
| 字段名 | 类型 | 必填 | 约束说明 |
|---|---|---|---|
plaintext |
string | 是 | 原始敏感数据,长度 ≤ 1024 字符,不可包含不可见字符 |
context.domain |
string | 是 | 业务域标识,枚举值:payment, identity, compliance |
context.user_id |
string | 否 | 关联用户,若提供则支持后续的令牌回收 |
context.expiration |
string | 否 | ISO 8601 格式,若不传则使用系统默认有效期 |
options._type |
string | 是 | PAN / UUID / RANDOM 枚举之一 |
options.lifetime_seconds |
int | 否 | 存活秒数,最小值 300,最大值 86400 |
options.reversible |
bool | 否 | 是否可逆,false 表示仅支持单向映射 |
注意:plaintext 绝不能直接在日志中输出。我曾见过团队把整个 request body 打到 access log 里,后果就是信用卡号明文落库。审计查出来直接红牌罚款。
4.4 响应字段详解
响应体我习惯分成三块:令牌本体、元数据、签名。这么设计调用方一眼就能找到自己想要的。
正常响应示例(HTTP 201):
{
"_id": "tok_eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MjYw...",
"": "4389-****-****-1234",
"metadata": {
"_type": "PAN",
"issued_at": "2024-09-15T10:30:00Z",
"expires_at": "2024-09-15T11:30:00Z",
"domain": "payment",
"user_id": "u_abc123"
},
"signature": "a1b2c3d4e5f6..."
}
| 字段 | 说明 | 是否必须 |
|---|---|---|
_id |
令牌唯一标识,用于后续查询和撤销 | 是 |
|
脱敏后的令牌值。可展示给终端用户 | 是 |
metadata |
令牌的上下文信息 | 是 |
signature |
对 _id + 的 HMAC 签名 |
强烈建议 |
我的习惯:signature 我用 HMAC-SHA256,密钥只存在令牌服务的机密存储里。这样哪怕调用方篡改了 _id,我们一验就能识破。
4.5 错误响应格式
写接口比写业务代码更重要的,是写好错误响应。我见过太多 "error: internal error" 这种废物信息。来,看看专业的怎么搞。
错误响应示例:
{
"error": {
"code": "INVALID_PLAINTEXT",
"message": "原始数据包含不可见字符(0x00)",
"field": "plaintext",
"request_id": "req_55e3f7a1",
"timestamp": "2024-09-15T10:30:01Z"
}
}
| 字段 | 说明 |
|---|---|
code |
错误码枚举,如 INVALID_PLAINTEXT、IDEMPOTENCY_CONFLICT、_TYPE_UNSUPPORTED |
message |
人类可读的描述,里面可以带具体异常值 |
field |
出错的字段名,方便调用方定位 |
request_id |
与请求 header 的 request-id 一致 |
timestamp |
服务器生成错误的时间 |
4.6 核心流程总览
我把整个生成流程画成一张图,你一眼就能看明白。
关于幂等性的一个小故事:我曾经在生产环境碰到一个客户重试导致生成两个不同令牌的 case。后来我加了一层幂等表,用 (idempotency_key, plaintext_hash) 做联合唯一索引,这种事再没发生过。
4.7 版本化与兼容性
接口一旦发布,就得负责。我建议:
- 路径里带版本号,比如
/api/v1/tokens,后面要改就走/api/v2/tokens - 响应里不要随便删字段。如果非得删,先用
deprecated标记警告一年 - 请求体新增字段时要确保老客户端不传也能正常工作
举个例子:我们 v1 的 context 里只有 domain。v2 新增了 user_id,就做成可选,旧请求不传也照常生成令牌。这么做,调用方升级没压力。
4.8 小结
令牌生成接口最讨厌的就是细节。幂等键落了,日志漏了,签名没算——每一个都能让你晚上被电话叫醒。按照本章的规范来做,至少能保证你的接口在规范上不输于人。
下回你要是自己手写一个生成接口,把这些参数一张表钉在墙上,少了哪项都能一眼看出来。嗯,就这么干。