2. 令牌化接口基础
聊令牌安全,必须先搞清楚令牌在哪儿跑、怎么跑。接口类型不同,令牌的传递姿势也完全不一样。我个人习惯把接口类型和令牌传递方式放在一起看——你想想看,一个 REST API 和一个 WebSocket 长连接,令牌的存活周期、传递位置、校验时机能一样吗?肯定不一样。
这一节咱们过一遍三种主流接口——REST、gRPC、WebSocket,以及令牌在它们身上的"藏身之处":Header、Cookie、Body。嗯,这里要注意,每种接口能用哪几种传递方式,是有讲究的。
2.1 接口类型概览
先简单看一眼三种接口的特征。我把它们最核心的差异列在下面:
| 接口类型 | 通信协议 | 消息格式 | 连接方式 | 典型场景 |
|---|---|---|---|---|
| REST | HTTP/1.1, HTTP/2 | JSON, XML | 短连接(请求-响应) | Web API、移动端后端 |
| gRPC | HTTP/2 | Protocol Buffers | 长连接 / 流式 | 微服务间通信、高性能后端 |
| WebSocket | WS / WSS | 文本 / 二进制帧 | 全双工长连接 | 实时推送、在线协作、游戏 |
REST 最常用,门槛低,但令牌暴露面也最大。gRPC 内部通信多,令牌通常走 Metadata。WebSocket 是长连接,令牌只在握手阶段传递一次,后续靠连接上下文维持。三种接口的安全关注点完全不一样。
2.2 令牌在 REST 接口中的传递
REST 接口的令牌传递方式最丰富,也最容易出问题。我在项目中遇到过好几次,因为选错了传递位置导致令牌被泄漏的情况。
Header 方式
最常见的方式。令牌放在 HTTP 请求头里,标准做法是用 Authorization: Bearer <>。我建议你们统一用这个格式,别自己发明头字段名。
GET /api/v1/user/profile HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Accept: application/json
Cookie 方式
令牌放在 Set-Cookie 里下发,客户端自动带回来。这种方式对前端友好,但容易受到 CSRF 攻击。我曾经在审计时发现一个系统把 JWT 放 Cookie 里,没设 HttpOnly 和 SameSite,结果被 XSS 一把梭走——嗯,从那以后我每次看 Cookie 传令牌都会多瞄两眼这两属性。
Set-Cookie: access_=eyJhbGciOiJIUzI1NiIs...; Path=/; HttpOnly; Secure; SameSite=Strict
HttpOnly(防 XSS 偷取)、Secure(只走 HTTPS)、SameSite=Strict/Lax(防 CSRF)。缺一个都是漏洞。
Body 方式
令牌放在 POST 请求的请求体里。这种方式不太标准,但在某些老系统或特定场景下还能见到。说白了,GET 请求没法用 Body 传令牌,所以适用范围有限。
POST /api/v1/auth/verify HTTP/1.1
Content-Type: application/json
{
"": "eyJhbGciOiJIUzI1NiIs...",
"action": "verify"
}
2.3 令牌在 gRPC 接口中的传递
gRPC 基于 HTTP/2,令牌传递主要走 Metadata。Metadata 类似于 HTTP 的 Header,也是键值对形式。你可以在客户端拦截器里统一注入令牌,服务端用拦截器统一校验——这样做的好处是业务代码里完全不用关心令牌逻辑。
// 客户端拦截器示例(Go)
func authUnaryInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
md := metadata.Pairs("authorization", "Bearer " + )
ctx = metadata.NewOutgoingContext(ctx, md)
return invoker(ctx, method, req, reply, cc, opts...)
}
// 服务端拦截器示例
func authServerInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Errorf(codes.Unauthenticated, "missing metadata")
}
tokens := md.Get("authorization")
if len(tokens) == 0 {
return nil, status.Errorf(codes.Unauthenticated, "missing ")
}
// 校验 ...
return handler(ctx, req)
}
gRPC 还支持流式调用,拦截器同样生效。我记得有一次排查一个 gRPC 服务间调用问题,发现服务 A 给服务 B 传令牌时,Metadata 的 key 大小写没统一——gRPC 默认把 Metadata key 转成小写,结果两边对不上,认证一直失败。你想想看,这种小细节排查起来多折腾人。
2.4 令牌在 WebSocket 接口中的传递
WebSocket 是长连接,令牌只能在一开始握手的时候传。一旦连接建立成功,后续所有帧都复用这个连接上下文,不会再传令牌。所以核心问题就是:握手阶段怎么安全地传递令牌?
主流做法有两种:
- URL 参数传令牌:
ws://example.com/socket?=eyJ...—— 简单但 URL 参数容易被服务器日志、浏览器历史记录泄漏。我个人不推荐在生产环境这么干。 - Header 传令牌: 在 WebSocket 握手时通过自定义 Header 传令牌。浏览器端的 WebSocket API 不支持自定义 Header,但服务端之间或客户端(如移动端、桌面端)可以用。
- Cookie 传令牌: 浏览器端最常用。WebSocket 握手时会自动带上目标域名的 Cookie,服务端从 Cookie 里取令牌校验。
// 服务端 WebSocket 握手校验(Node.js + ws 库示例)
const WebSocket = require('ws');
const server = new WebSocket.Server({ port: 8080 });
server.on('connection', (ws, req) => {
// 从 Cookie 中取令牌
const cookies = req.headers.cookie || '';
const = cookies.split(';').find(c => c.trim().startsWith('access_='));
if (!) {
ws.close(4001, 'Unauthorized');
return;
}
// 校验 ...
console.log('WebSocket 连接已认证');
});
2.5 令牌传递方式对比
三种接口、三种传递方式,怎么选?我把它们的核心差异总结成一张表,方便你对照:
| 传递方式 | 支持的接口类型 | 安全性 | 浏览器兼容性 | 典型风险 |
|---|---|---|---|---|
| Header | REST、gRPC、WebSocket(服务端) | 高(仅 HTTPS) | 良好(fetch/XHR 支持) | HTTPS 配置不当会泄漏 |
| Cookie | REST、WebSocket(浏览器) | 中(依赖 HttpOnly/SameSite) | 原生支持 | CSRF、XSS 偷取 |
| Body | REST(POST/PUT) | 低 | 良好 | 不支持 GET、日志泄漏 |
| Metadata | gRPC | 高(HTTP/2 加密) | 不适用 | Key 大小写歧义 |
宏观上看,Header 是通用性最强的方案,适合 REST 和 gRPC。Cookie 在浏览器场景下绕不开,但安全配置必须做全。Body 嘛——我建议除非有特殊原因,否则尽量别用。
下面我把本章的核心知识体系整理成一张框架图,你可以用它来快速回顾:
核心要点回顾:
- REST 接口令牌传递方式最多(Header / Cookie / Body),但也最容易出配置漏洞。优先用 Header + Bearer 方案。
- gRPC 走 Metadata,靠拦截器统一注入和校验,注意 Key 大小写问题。
- WebSocket 只在握手阶段传一次令牌,后续靠连接上下文维持。Cookie 是浏览器端最靠谱的方式。
- 无论哪种接口,令牌传递必须走加密通道(HTTPS / WSS),这是底线,没得商量。
一个小建议:你在设计新系统时,先确定接口类型,再选令牌传递方式,然后围绕它配安全策略。顺序别搞反了——我见过有人先选了 Cookie 传令牌,然后发现接口是 gRPC 的,结果 Metadata 用不了,又回去改架构,折腾得够呛。