外观
API 网关鉴权体系分层设计:JWT、OAuth2 与内网 mTLS 的边界划分
问题背景:两种极端都有事故买单
一类团队的鉴权是「单点防御」:网关校验完 JWT,内网服务之间裸奔互信,攻击者只要打穿边缘一个点,内网横向移动如入无人之境;另一类是「层层加码」:每个服务各自实现一遍鉴权逻辑,有的校验签名、有的只解 payload、有的连过期都不看,五套实现三种算法,审计时谁也说不清到底哪层真正生效。健康的鉴权体系应该是分层的纵深防御:边缘做统一认证(你是谁)、内网做服务身份认证(这个调用来自哪个可信工作负载)、业务层做细粒度授权(你能做什么)。三层各司其职,才既不重复建设,也不留下裸奔地带。这套分层打法的核心,是让认证语义与授权语义互不越界:认证结果标准化传递,授权决策在离数据最近的一层做出。
方案对比:每层的候选方案与适用边界
边缘层(南北向):JWT 自包含、无状态校验快,适合高并发读接口,但吊销难;OAuth2/OIDC 是协议框架,配合授权服务器实现登录、token 签发与刷新;opaque token(不透明令牌)每次回查授权服务器,天然支持即时吊销,代价是多一跳网络。主流组合是「短效 JWT 访问令牌 + 长效 refresh token + 服务端黑名单兜底」。服务间(东西向):mTLS 双向证书认证是事实标准,K8s 环境用 Istio 的 PeerAuthentication 或 cert-manager 自动签发轮换;SPIFFE/SPIRE 适合跨集群、多身份体系的大规模场景。授权层:RBAC 管角色权限足够应付多数业务,涉及「本人只能看自己的订单」「华东账号只能看华东数据」这类属性判断时,才需要引入 ABAC 或把授权下沉到服务内判断。
实操步骤:把三层防线依次立起来
第一步:网关统一校验 JWT。 以 Spring Cloud Gateway 为例,用资源服务器方式接入 JWKS 公钥,校验签名与过期全部内建:
java
@Bean
public SecurityWebFilterChain gatewaySecurity(ServerHttpSecurity http) {
return http
.csrf(ServerHttpSecurity.CsrfSpec::disable) // 网关层无浏览器会话,CSRF 关闭
.authorizeExchange(ex -> ex
.pathMatchers("/api/login", "/api/captcha").permitAll()
.anyExchange().authenticated())
.oauth2ResourceServer(rs -> rs.jwt(jwt -> jwt
// 从授权服务器拉取公钥集,签名、exp、nbf 自动校验
.jwkSetUri("https://sso.example.com/.well-known/jwks.json")
.jwtDecoder(jwtDecoder())))
.build();
}
// 显式锁定允许的算法,防 alg=none 与 RS/HS 混淆攻击
ReactiveJwtDecoder jwtDecoder() {
NimbusReactiveJwtDecoder decoder = NimbusReactiveJwtDecoder
.withJwkSetUri("https://sso.example.com/.well-known/jwks.json").build();
decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(
new JwtTimestampValidator(Duration.ofSeconds(30)),
new JwtIssuerValidator("https://sso.example.com")));
return decoder;
}校验通过后,网关把用户身份转成内部请求头(如 X-User-Id、X-Tenant-Id)向下游传递,并剥离外部请求携带的同名头,防止伪造身份注入。内部身份传递要有一套完整的防伪造约定,通常包含三件事:剥离外部同名头(防止攻击者伪造 X-User-Id 直连内网服务)、补充签名(网关对注入头做 HMAC 签名,服务端验签后才信任身份)、声明流量来源(外部用户流量与内部服务间调用分开标记)。做到这三点,内网服务才敢放心信任身份头;任何一步缺失,身份头都等于一个可伪造的口令。
第二步:JWT 吊销与刷新策略落地。 访问令牌 TTL 压到 5~15 分钟,敏感操作(改密码、支付)前用 jti 查 Redis 黑名单做二次确认;refresh token 只在授权服务器签发与轮换,发现旧 token 被复用即触发整条令牌族作废。
第三步:内网 mTLS 覆盖东西向流量。 最小代价路径是服务网格层开启严格 mTLS,无需改一行业务代码:
yaml
# Istio PeerAuthentication:网格内强制双向 TLS
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
name: default
namespace: prod
spec:
mtls:
mode: STRICT # 明文流量直接拒绝,完成内网身份强制证书签发与轮换由网格控制面自动完成(默认 24 小时轮换),业务服务零感知;未接入网格的服务用 ingress 网关 mTLS + cert-manager 自签 CA 过渡。第四步:把授权规则写进接口契约。 网关注释里明确每个路由的鉴权级别(公开/登录/角色/内部),代码评审时新增路由必须先过这道检查,防止「先上线后补权限」成为习惯。授权模型的落点也要选对层:RBAC 的角色判断放在网关层(路由级,如 /admin/** 需要管理员角色)能挡住八成的粗粒度越权,但「本人只能改自己的地址」这类资源级判断必须留在业务服务里——网关看不到数据归属,强行在网关做资源级授权只会催生一堆绕路接口。经验法则是:网关管「能不能进这个门」,服务管「进门后能不能动这个资源」,两层判断共用同一套权限中台数据,避免规则在两层之间漂移。
避坑总结
- JWT 校验必须锁定算法白名单。 只解不验、信任
alg头、允许 none,是最经典的鉴权绕过;解码与验签用成熟库的「先验签后取值」接口,禁止手搓。 - JWT 里只放身份标识,不放敏感数据。 payload 仅 base64 编码并非加密,手机号、身份证一旦写入,任何持有 token 的终端都能解开;登录态、权限快照在服务端按需查询。
- 网关校验不是内网裸奔的理由。 零信任视角下内网流量同样要 mTLS 做工作负载认证,至少做到「网关传来的身份头只能由网关注入」这一条硬约束。
- 黑名单 TTL 与 JWT 剩余有效期对齐。 黑名单存到 token 自然过期即可删除,TTL 过长白白占用内存,过短则吊销形同虚设。
- 证书过期要纳入告警体系。 mTLS 证书过期引发的连环 503 比证书泄漏更常见,cert-manager 自动轮换 + 到期前 14 天告警是底线配置。
收尾
鉴权体系设计的判断标准很简单:任何一层被绕过,下一层都能把攻击挡住,且每一层的策略都能被审计复述出来。边缘管认证、内网管身份、业务管授权,三层语义不重叠、实现不重复。把它固化成网关路由模板与脚手架默认值,让新服务从出生起就在防线之内——安全建设的最高境界,是让正确的事情成为最省力的事情。
免责声明
本工具与文章内容仅用于学习测试场景,优化建议仅供参考,请勿直接用于生产环境,由此产生的风险自行承担。