简介:鉴权是微服务体系的第一道安全关口。本文系统拆解 JWT 无状态令牌与 Token+分布式缓存有状态会话两大主流方案,从登录签发、网关校验、报文注入到下游消费的完整链路展开,对比可注销性、权限时效、性能开销与容灾成本,并给出典型报文结构与场景化选型建议,帮助你在架构设计时做出合适决策。
在微服务架构下,系统被拆分为多个独立服务,用户的一次请求往往要跨越网关、用户服务、业务服务等多个节点。“用户是谁、是否登录、有没有权限”这三个问题必须在入口处统一回答,否则每个服务都各自实现一遍鉴权,既重复又容易出漏洞。业界的主流做法是在 API 网关集中鉴权,而网关背后有两种经典流派:
两者没有绝对高下,差异的本质是“状态放在令牌里”还是“状态放在服务端”。下面分别拆解。
JWT 由三段 Base64Url 编码的文本用点号拼接而成:
base64(header).base64(payload).base64(signature)
示例(载荷脱敏):
// header{"alg": "HS256", "typ": "JWT"}// payload{"sub": "u-88123", "iss": "auth-svc", "iat": 1733712000, "exp": 1733719200}
签名部分是对前两段做 HMAC-SHA256(或 RSA/ECDSA 私钥签名)的结果。网关拿到令牌后本地验签,签名正确且未过期,即信任载荷中的身份声明,全程不需要查询任何存储。
这是另一条经典路线,也是不少中大型平台内部广泛使用的方案。整体角色包括:API 网关(总入口)、用户/登录服务(签发与校验)、分布式缓存(会话存储)、若干业务微服务。
Set-Cookie: access_token_id=xxxx(HttpOnly、Secure)。示意伪代码:
def login(username, password, ctx):user = verify_credentials(username, password) # 校验账号口令token = secure_random_hex(32) # 256-bit 不透明令牌session = {"userID": user.id,"ctime": now(),"clientIp": ctx.ip,"region": ctx.region,"device": ctx.ua_hash,}cache.set("session:" + token, json.dumps(session), ttl=7200)resp.set_cookie("access_token_id", token, http_only=True, secure=True)return {"expires_in": 7200}
此后前端每次调用,携带 access_token_id 请求头或 Cookie。网关的处理链路如下:
userInfo 字段,同时把网关公共信息(出口节点 IP 列表、环境标识等)补充到 commons 字段,再转发给业务微服务;userInfo 字段:字段存在即已登录,字段内容即当前用户——下游编码极其简单。
def on_request(ctx):token = ctx.header.get("access_token_id") or ctx.cookie.get("access_token_id")if not token:return abort(401)user = user_service.introspect(token, ctx.ip, ctx.region)if user is None:return abort(401) # 未登录 / 会话过期 / 风控拦截ctx.body.pop("userInfo", None) # 剥离客户端伪造字段,防止串号ctx.body["userInfo"] = userctx.body["commons"] = {"apiGw": ctx.gateway_nodes, "env": ctx.env}forward(ctx)
一个典型的业务请求体结构(数据脱敏)如下:
{"userInfo": {"userID": "uxM11-wx177588","ip": ["179.8.16.66", "179.8.16.1", "166.19.10.1"],"ctime": "2024-12-09 09:56:55"},"commons": {"apiGw": ["10.0.96.17", "10.0.96.18", "10.0.96.19"],"env": {}},"data": {"typeName": "Person","mapper": {"nodeType": "Actor", "nodeLabel": "Person"}}}
userInfo 或 userID,也会被网关剥离覆盖,越权风险从入口被堵死;| 对比维度 | JWT | Token + 分布式缓存 |
|---|---|---|
| 校验位置 | 网关本地验签 | 集中查询缓存 |
| 主动失效 | 困难,需额外黑名单 | 删除 key 即时失效 |
| 权限变更时效 | 需等令牌过期 | 即时生效 |
| 令牌信息量 | 自包含、明文可读 | 随机串、无信息 |
| 每请求开销 | CPU 验签 + 大载荷传输 | 一次缓存查询 |
| 基础设施依赖 | 无(仅需分发密钥) | 强依赖缓存高可用 |
| 多端会话管理 | 弱 | 强,可枚举、可踢下线 |
| 典型场景 | 开放 API、跨域、无状态扩展 | 内部系统、强管控与风控 |
观察多家头部云厂商的开放 API,会发现大量接口采用 POST + 统一 JSON 报文,而非严格的 REST 风格。这并非“不会用 REST”,而是工程上的综合权衡,其中与鉴权直接相关的因素包括:
userInfo、commons 写进请求体,统一的 POST JSON 报文天然适合“公共参数 + 业务参数”的包裹结构;当然,REST 在资源型操作、HTTP 缓存语义上仍有不可替代的价值,两者是场景选择而非对错之分。
给出一个可操作的决策路径:
无论选哪条路线,以下几件事必须做到位:
userInfo 前先删除客户端传入的同名字段,这是防越权的关键细节,也是这类方案容易被忽视的坑;JWT 与 Token + 分布式缓存的争论,本质是“信任令牌本身”还是“信任服务端状态”。JWT 换来的是无状态的简洁与横向扩展能力,代价是失去对已签发令牌的控制力;Token + 缓存换来的是随时可回收的强管控与更简单的下游编码,代价是对缓存基础设施的依赖与每次请求的一跳查询开销。对大多数企业内部系统而言,会话可控性的价值高于一次本地验签的节省;而对开放、跨域、无状态场景,JWT 更为契合。理解两者的能力边界,按场景组合使用,比争论“哪个更好”更有意义。