Gateway统一认证、OAuth2与微服务授权
微服务安全不能简单理解成“Gateway验一下Token,后端服务全部放行”。Gateway适合做统一入口认证、路由级粗授权、Header清洗、Token透传和审计入口;每个业务服务仍然要作为资源服务器校验身份和权限,并在Service层做业务数据权限。
本页把一个商业系统中常见的OAuth2/OIDC登录、JWT校验、Gateway统一认证、服务间授权、Feign透传、方法授权和排查流程串起来。
一、先分清几个概念
| 概念 | 解决什么 | 常见落地 |
|---|---|---|
| Authentication认证 | 你是谁 | 登录、验JWT、验Session、验客户端证书 |
| Authorization授权 | 你能做什么 | URL权限、方法权限、数据权限、租户权限 |
| OAuth2 | 授权委托框架 | 第三方授权、统一认证中心签发Access Token |
| OIDC | 在OAuth2之上补身份认证 | 登录后拿到ID Token和用户身份声明 |
| Authorization Server | 认证授权中心 | Keycloak、Authing、自建Spring Authorization Server |
| Resource Server | 资源服务器 | Gateway、订单服务、库存服务校验Access Token |
| Client | 代表用户或系统发起授权的应用 | 前端、Gateway BFF、后台服务、移动端 |
| Access Token | 访问资源的凭证 | JWT或Opaque Token |
| Refresh Token | 换取新Access Token | 通常只给受信任客户端保存 |
OAuth2严格说主要解决“授权”,OIDC才补齐“登录认证”。企业项目里常说“OAuth2登录”,通常实际是OAuth2 + OIDC。
二、推荐总体架构
flowchart TD
A["浏览器或App"] --> B["Gateway统一入口"]
B --> C["认证授权中心<br/>OAuth2/OIDC"]
B --> D["订单服务<br/>Resource Server"]
B --> E["库存服务<br/>Resource Server"]
D --> F["用户权限库或IAM"]
E --> F推荐边界:
| 层 | 做什么 | 不应该做什么 |
|---|---|---|
| 认证授权中心 | 登录、授权码、发Token、密钥轮换、用户会话 | 不处理订单库存业务 |
| Gateway | 统一入口认证、Token校验、粗粒度路由授权、Header清洗、限流、审计 | 不做复杂业务权限和数据库事务 |
| 业务服务 | 再次校验Token、方法权限、数据权限、业务审计 | 不盲目信任外部传入的用户Header |
| 数据层 | 用租户、组织、资源归属约束数据 | 不只靠前端隐藏按钮防越权 |
核心原则:
Gateway可以减少重复入口认证,但不能替代每个服务自己的授权边界。任何服务只要能被网络访问,就必须假设有人可能绕过Gateway直接请求它。
三、OAuth2/OIDC登录流程
前后端分离或浏览器后台系统最常见是授权码模式,公开客户端还应使用PKCE。
flowchart TD
A["用户访问前端或Gateway"] --> B["未登录,跳转认证中心"]
B --> C["用户输入账号密码或扫码"]
C --> D["认证中心完成认证和授权"]
D --> E["回调返回Authorization Code"]
E --> F["客户端用Code换Token"]
F --> G["拿到Access Token和可选ID Token"]
G --> H["请求Gateway携带Bearer Token"]
H --> I["Gateway验签和校验声明"]
I --> J["转发到业务服务"]几个Token要区分:
| Token | 给谁用 | 作用 | 是否应该传给业务服务 |
|---|---|---|---|
| Access Token | 资源服务器 | 访问API | 是,通常传给Gateway和后端服务 |
| ID Token | 客户端 | 表示用户登录身份 | 通常不作为API访问凭证 |
| Refresh Token | 客户端 | 换新Access Token | 不应该在微服务间到处传 |
如果是纯后端管理系统,常见做法是Gateway作为OAuth2 Client完成登录,再通过Session保存登录态,这种模式接近BFF。浏览器只和Gateway会话交互,Access Token不暴露给前端JavaScript,安全性更好。
四、两种常见模式怎么选
4.1 Token直传模式
前端登录后拿到Access Token,请求时携带:
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...Gateway校验后再转发给下游,下游服务也校验Token。
flowchart TD
A["前端保存Access Token"] --> B["请求Gateway携带Bearer"]
B --> C["Gateway作为Resource Server验JWT"]
C --> D["Token继续透传到业务服务"]
D --> E["业务服务再次验JWT和权限"]优点是简单,适合App、开放API、前后端分离。风险是Token暴露在浏览器环境,必须控制有效期、防XSS、避免把敏感信息放进JWT。
4.2 Gateway BFF模式
Gateway作为OAuth2 Client完成授权码登录,浏览器只持有HttpOnly Cookie Session。Gateway调用下游时由Token Relay带上Access Token。
flowchart TD
A["浏览器只有HttpOnly Session Cookie"] --> B["Gateway保存授权客户端信息"]
B --> C["Gateway通过TokenRelay转发Access Token"]
C --> D["业务服务作为Resource Server验Token"]优点是Access Token不直接暴露给前端JS,更适合企业后台。代价是Gateway有会话状态,要考虑Session存储、扩容、CSRF、退出登录和多端管理。
五、Gateway作为资源服务器校验JWT
适合Token直传模式。Gateway只要验证Access Token签名、过期时间、issuer、audience和scope,就可以在入口拒绝无效请求。
5.1 依赖示例
Java 17 + Spring Boot 3.x常见依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>JDK 8 + Boot 2.x老项目要按项目BOM选择兼容版本,并注意javax到jakarta的差异。不要把Boot 3配置直接复制到Boot 2老项目。
5.2 Gateway配置
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://auth.example.com/realms/medicalissuer-uri会让应用读取授权服务器的OIDC元数据和JWK公钥地址。生产上要保证授权中心高可用,并设置合理缓存;密钥轮换时新旧Key要有重叠窗口。
5.3 WebFlux安全配置
Gateway基于WebFlux,使用SecurityWebFilterChain。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
@Configuration
public class GatewaySecurityConfig {
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
return http
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.authorizeExchange(exchange -> exchange
.pathMatchers("/actuator/health", "/auth/callback").permitAll()
.pathMatchers(HttpMethod.GET, "/api/public/**").permitAll()
.pathMatchers("/api/admin/**").hasAuthority("SCOPE_admin")
.pathMatchers("/api/orders/**").hasAnyAuthority("SCOPE_order:read", "SCOPE_order:write")
.anyExchange().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt())
.build();
}
}这段配置做了三件事:
- 健康检查和公开接口放行。
- 管理接口要求
adminscope。 - 其他请求必须携带有效JWT。
注意:Gateway这里做的是入口粗授权,不是完整业务授权。比如order:read只能说明用户可访问订单查询接口,不能说明他能看所有医院、所有租户、所有订单。
六、Gateway作为OAuth2 Client登录并TokenRelay
适合BFF模式。Gateway负责发起OAuth2登录,登录成功后把Access Token转发给下游。
6.1 配置OAuth2 Client
spring:
security:
oauth2:
client:
registration:
medical-gateway:
provider: medical-auth
client-id: medical-gateway
client-secret: ${AUTH_CLIENT_SECRET}
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
scope:
- openid
- profile
- order:read
- order:write
provider:
medical-auth:
issuer-uri: https://auth.example.com/realms/medical6.2 开启oauth2Login
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
@Configuration
public class GatewayLoginSecurityConfig {
@Bean
public SecurityWebFilterChain gatewayLoginChain(ServerHttpSecurity http) {
return http
.authorizeExchange(exchange -> exchange
.pathMatchers("/actuator/health", "/login/**").permitAll()
.anyExchange().authenticated()
)
.oauth2Login(oauth2 -> { })
.oauth2Client(oauth2 -> { })
.build();
}
}6.3 路由中使用TokenRelay
spring:
cloud:
gateway:
routes:
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/orders/**
filters:
- TokenRelay=
- StripPrefix=1TokenRelay会把当前登录用户关联的Access Token放到转发请求中。下游服务仍按资源服务器方式验Token。
七、业务服务怎样认证和授权
每个业务服务建议也配置成Resource Server。这样即使绕过Gateway访问服务,只要没有合法Token,也会被拒绝。
7.1 Servlet服务JWT校验
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://auth.example.com/realms/medicalimport org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableMethodSecurity
public class ResourceServerSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt())
.build();
}
}7.2 方法级授权
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OrderController {
@GetMapping("/orders/{orderId}")
@PreAuthorize("hasAuthority('SCOPE_order:read')")
public OrderVO getOrder(@PathVariable Long orderId) {
return orderService.getOrder(orderId);
}
}方法级授权解决“有没有订单查询权限”,但仍不能解决“能不能看这张订单”。数据权限应放在Service层。
7.3 数据权限必须在Service层做
public OrderVO getOrder(Long orderId) {
CurrentUser user = CurrentUserHolder.get();
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new NotFoundException("订单不存在"));
if (!permissionService.canAccessHospital(user, order.getHospitalId())) {
throw new AccessDeniedException("无权访问该医院订单");
}
return mapper.toVO(order);
}为什么不能只靠Gateway?
| 只靠Gateway的问题 | 后果 |
|---|---|
| Gateway只知道URL,不知道数据库资源归属 | 用户有接口权限但越权看别人的订单 |
| 内部服务可能被绕过Gateway访问 | Header伪造或内网误开放导致越权 |
| 权限变化不一定实时反映到Token | 旧Token仍带旧权限 |
| 批量接口、导出接口粒度复杂 | URL权限无法表达每条数据归属 |
八、权限声明怎么设计
JWT里建议只放稳定、必要、低敏感声明:
{
"iss": "https://auth.example.com/realms/medical",
"sub": "10001",
"aud": ["medical-api"],
"scope": "order:read order:write asset:read",
"tenant_id": "t001",
"org_id": "hospital-001",
"token_version": 7,
"exp": 1890000000
}不要放手机号、身份证、详细患者数据、银行卡、密钥这类敏感信息。JWT默认是签名,不是加密,拿到Token的人通常可以解码payload。
常见权限分层:
| 权限层 | 示例 | 判断位置 |
|---|---|---|
| 登录态 | 是否有效Token | Gateway和各服务 |
| 路由权限 | 能否访问/api/admin/** | Gateway |
| 接口权限 | order:read、order:refund | Controller或方法授权 |
| 数据权限 | 医院、科室、租户、资源归属 | Service和SQL条件 |
| 字段权限 | 是否能看身份证、手机号 | DTO组装、脱敏组件 |
| 操作审计 | 谁导出了患者数据 | 审计日志和风控 |
九、Gateway向下游传什么Header
不要把外部请求头原样透传给下游。Gateway应先清洗,再覆盖内部可信Header。
推荐:
| Header | 来源 | 用途 |
|---|---|---|
Authorization | 原始Access Token或TokenRelay | 下游资源服务器验Token |
X-Trace-Id | Gateway生成或沿用 | 链路追踪 |
X-User-Id | Gateway验Token后覆盖写入 | 便捷审计,不能单独作为认证依据 |
X-Tenant-Id | Gateway验Token后覆盖写入 | 租户路由和日志 |
X-Request-Id | Gateway生成 | 幂等、排查 |
危险做法:
客户端传 X-User-Id: 1
Gateway原样转发
下游相信 X-User-Id正确做法是:下游以JWT验签结果为准,Header只作为辅助上下文;如果确实要信任Gateway写入的内部Header,服务间网络必须隔离,并且Gateway要删除外部同名Header后重新写。
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class UserHeaderFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return exchange.getPrincipal()
.cast(Authentication.class)
.flatMap(authentication -> {
ServerHttpRequest.Builder builder = exchange.getRequest().mutate()
.headers(headers -> {
headers.remove("X-User-Id");
headers.remove("X-Tenant-Id");
});
builder.header("X-User-Id", authentication.getName());
return chain.filter(exchange.mutate().request(builder.build()).build());
})
.switchIfEmpty(chain.filter(exchange.mutate()
.request(exchange.getRequest().mutate()
.headers(headers -> {
headers.remove("X-User-Id");
headers.remove("X-Tenant-Id");
})
.build())
.build()));
}
@Override
public int getOrder() {
return -100;
}
}这里使用switchIfEmpty处理未认证请求,并且无论是否已认证都先删除外部传入的同名Header,避免客户端伪造身份。
十、服务间调用怎样授权
服务间调用分两类。
10.1 代表用户调用下游
订单服务调用库存服务时,如果库存操作需要知道“哪个用户触发”,可以透传用户Access Token。
flowchart TD
A["用户请求订单服务"] --> B["订单服务读取当前JWT"]
B --> C["Feign RequestInterceptor透传Authorization"]
C --> D["库存服务验JWT和权限"]Feign拦截器示例:
import feign.RequestInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
@Configuration
public class FeignAuthRelayConfig {
@Bean
public RequestInterceptor authorizationRelayInterceptor() {
return template -> {
var authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication instanceof JwtAuthenticationToken jwtAuth) {
String token = jwtAuth.getToken().getTokenValue();
template.header("Authorization", "Bearer " + token);
}
};
}
}JDK 8项目不要使用var和模式匹配写法,改成显式类型和强转即可。
10.2 系统自己调用下游
定时任务、MQ消费者、批处理没有用户上下文,应使用client_credentials获取系统Token,而不是伪造某个管理员用户。
flowchart TD
A["批处理服务"] --> B["用client_id和client_secret换Token"]
B --> C["拿到服务账号Access Token"]
C --> D["调用资产服务"]
D --> E["资产服务校验client权限"]服务账号权限要最小化。例如采集任务只需要asset:write,不应该拥有user:delete。
十一、JWT和Opaque Token怎么选
| 方案 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| JWT | 本地验签快,不必每次查认证中心 | 撤销复杂,声明变更不实时,泄露后有效期内可用 | 高并发API、微服务内部 |
| Opaque Token | 服务端可实时撤销和集中判断 | 每次可能要introspection,依赖认证中心 | 高安全场景、权限变化频繁 |
JWT生产建议:
- 使用非对称签名,例如RS256或ES256。
- 校验
iss、aud、exp、nbf。 - Access Token短有效期。
- Refresh Token只保存在可信客户端或服务端。
- 支持Key Rotation。
- 敏感数据不进payload。
- 权限变化用
token_version、黑名单或短Token窗口控制。
11.1 JWK公钥缓存和密钥轮换
JWT验签通常不是每次请求都远程调用认证中心。资源服务器会从issuer-uri发现OIDC元数据,再拿到jwks_uri,下载一组JWK公钥并缓存在本地。每次请求到来时,根据JWT Header里的kid在本地JWK缓存中找公钥验签。
flowchart TD
A["请求携带JWT"] --> B["读取Header中的kid"]
B --> C{"本地JWK缓存有kid吗"}
C -- "有" --> D["使用对应公钥验签"]
C -- "没有" --> E["刷新JWK集合"]
E --> F{"刷新后找到kid吗"}
F -- "有" --> D
F -- "没有" --> G["返回401"]为什么要缓存?因为验签是每个请求都会做的高频动作,如果每次都调用认证中心取公钥,认证中心会变成所有接口的同步瓶颈。一旦认证中心抖动,所有业务请求都会被拖慢甚至失败。
密钥轮换时最容易出事故的是“认证中心已经用新私钥签Token,但资源服务器还没有新公钥”。正确轮换必须有重叠窗口:
| 阶段 | 认证中心做什么 | 资源服务器看到什么 |
|---|---|---|
| 准备阶段 | 发布新公钥到JWK集合,但仍用旧私钥签Token | 缓存刷新后同时有旧kid和新kid |
| 切换阶段 | 开始用新私钥签Token,旧公钥继续保留 | 新旧Token都能验签 |
| 观察阶段 | 等旧Access Token自然过期 | 仍保留旧公钥,避免旧Token突然全401 |
| 清理阶段 | 旧Token过期后移除旧公钥 | 只保留新kid |
如果直接删除旧公钥,会导致旧Token在未过期前全部验签失败;如果只切换私钥但没有提前发布新公钥,会导致新Token全部401。生产上要把kid、issuer、audience、JWK刷新时间和验签失败原因打到安全日志里,否则只能看到“用户突然登录失效”。
排查401时特别关注:
| 证据 | 说明 |
|---|---|
JWT Header里的kid | 判断使用哪把钥匙签发 |
| 资源服务器JWK缓存更新时间 | 判断是否拿到了新公钥 |
| 认证中心当前JWK集合 | 判断新旧公钥是否同时发布 |
Token的iss和aud | issuer或audience错也会验签后被拒 |
| 网关和下游服务配置 | Gateway能过但下游401,常是issuer/audience/JWK缓存不同 |
11.2 Token撤销和权限变更为什么不一定实时生效
JWT是自包含Token。资源服务器本地验签通过后,就能读取其中的用户、scope、租户和过期时间。它的好处是高性能,坏处是撤销不天然实时。
flowchart TD
A["管理员收回用户权限"] --> B["权限库已更新"]
B --> C["用户旧JWT仍未过期"]
C --> D["资源服务器本地验签通过"]
D --> E["Token中旧scope仍可被读取"]
E --> F["可能继续访问旧权限"]解决方案不是单一开关,而是按安全等级组合:
| 方案 | 原理 | 优点 | 代价 |
|---|---|---|---|
| 短Access Token | 让旧权限最多存活几分钟 | 简单、压力小 | 无法秒级撤销 |
| Refresh Token服务端受控 | 续期时重新加载权限 | 权限最终收敛 | 需要会话和刷新流程 |
token_version | Token带版本,资源服务器校验版本是否仍有效 | 改密、禁用、收权可立即失效 | 每次或缓存查询版本 |
| 黑名单/撤销表 | 记录被撤销的jti或Token摘要 | 针对单Token撤销 | 高并发要缓存和过期清理 |
| Opaque Token introspection | 每次向认证中心或缓存查询Token状态 | 撤销强一致性更好 | 认证中心成为在线依赖 |
| 关键权限实时查 | Token只放身份,权限从缓存或库读取 | 权限变更可控 | 多一次缓存/DB访问 |
商业系统通常这样折中:
- Access Token设置短有效期,例如5到30分钟,具体看风险。
- Refresh Token只由可信客户端或Gateway BFF保存。
- JWT里放
sub、tenant_id、少量稳定scope和token_version,不放敏感数据。 - 高风险接口,例如导出医疗数据、退款、删除资产、修改权限,服务端实时查权限缓存和数据权限。
- 用户禁用、改密码、权限收回时,递增
token_version并删除权限缓存。 - 所有服务校验Token版本时使用本地短缓存,避免每次请求都打数据库。
JDK 8 Demo:用tokenVersion让旧Token失效。
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class TokenVersionDemo {
static class JwtClaims {
final String userId;
final int tokenVersion;
final String scope;
JwtClaims(String userId, int tokenVersion, String scope) {
this.userId = userId;
this.tokenVersion = tokenVersion;
this.scope = scope;
}
}
static class UserSecurityState {
private final Map<String, Integer> latestVersion = new ConcurrentHashMap<String, Integer>();
void loginInit(String userId) {
latestVersion.put(userId, 1);
}
void revokeAllTokens(String userId) {
latestVersion.put(userId, latestVersion.get(userId) + 1);
}
boolean isTokenStillValid(JwtClaims claims) {
Integer current = latestVersion.get(claims.userId);
return current != null && current.intValue() == claims.tokenVersion;
}
}
public static void main(String[] args) {
UserSecurityState state = new UserSecurityState();
state.loginInit("u1001");
JwtClaims oldToken = new JwtClaims("u1001", 1, "asset:read");
System.out.println(state.isTokenStillValid(oldToken));
state.revokeAllTokens("u1001");
System.out.println(state.isTokenStillValid(oldToken));
}
}这个Demo只演示版本思想。生产中latestVersion应放在用户安全状态表或Redis缓存中,并设置缓存过期、变更通知和降级策略。关键点是:资源服务器不能只相信旧JWT里的权限快照,还要能在权限变化时让旧Token尽快失效。
十二、各服务到底要不要再次认证
建议要。
flowchart TD
A["外部请求"] --> B["Gateway验Token"]
B --> C["转发到订单服务"]
C --> D["订单服务再次验Token"]
D --> E["方法权限判断"]
E --> F["Service数据权限判断"]只在Gateway认证的风险:
| 风险 | 例子 | 防护 |
|---|---|---|
| 绕过Gateway | 内网地址被误暴露 | 服务也作为Resource Server |
| Header伪造 | 伪造X-User-Id | 删除外部Header,服务验JWT |
| 网关规则漏配 | 新路由忘记鉴权 | 后端默认拒绝未认证 |
| 权限粒度不足 | 有接口权限但无数据权限 | Service层校验资源归属 |
| 内部横向移动 | 某服务被攻破后访问其他服务 | mTLS、网络策略、服务账号最小权限 |
如果出于性能考虑只让Gateway验JWT,下游至少要处在严格内网隔离下,并验证来自Gateway的mTLS证书或内部签名Header。但这属于有条件信任,不适合作为默认方案。
十三、401和403怎么在网关与服务中区分
| 状态 | 含义 | 常见原因 |
|---|---|---|
| 401 | 未认证或Token无效 | 没带Token、Token过期、签名错误、issuer不对 |
| 403 | 已认证但权限不足 | 缺少scope、角色不足、数据权限不满足 |
| 429 | 被限流 | QPS、并发、热点参数超阈值 |
| 502/503 | 网关或下游不可用 | 无实例、连接失败、熔断降级 |
不要把Token过期和无权限都返回同一个“系统错误”。前端、运维和安全审计需要明确区分。
十四、生产排查流程
flowchart TD
A["访问接口失败"] --> B["先看状态码"]
B --> C["401查Token和认证中心"]
B --> D["403查权限和数据范围"]
B --> E["429查限流规则"]
B --> F["503查路由、实例和熔断"]
C --> G["检查Bearer、过期、iss、aud、kid、公钥"]
D --> H["检查scope、角色、方法注解、资源归属"]
E --> I["检查Gateway和Sentinel规则"]
F --> J["检查Nacos、LoadBalancer、下游健康"]14.1 401排查
- 请求是否携带
Authorization: Bearer xxx。 - Token是否过期。
- JWT的
iss是否和配置的issuer-uri一致。 - JWT的
aud是否包含当前API。 - JWT Header的
kid是否能在JWK集合中找到。 - 授权中心公钥是否轮换但服务未刷新。
- Gateway是否把Authorization转发给下游。
- 代理或Nginx是否删除了Authorization Header。
14.2 403排查
- 当前用户是否认证成功。
- JWT里是否有需要的
scope或权限。 - Spring Security权限前缀是否匹配,例如
SCOPE_order:read。 - 方法上的
@PreAuthorize表达式是否正确。 - 数据权限是否拒绝,例如租户、医院、科室不匹配。
- 权限刚变更但旧Token仍未过期。
14.3 Gateway认证通过但下游401
常见原因:
| 原因 | 说明 |
|---|---|
| Gateway没有透传Authorization | 下游拿不到Token |
| TokenRelay未生效 | BFF模式下授权客户端信息没有关联 |
| 下游issuer配置不同 | Gateway和服务校验的认证中心不一致 |
| 下游audience更严格 | Token不是发给该资源服务的 |
| Header被Strip或重写 | 路由Filter或代理清掉了Header |
十五、商业落地建议
- 认证中心独立建设,不把登录逻辑散落到每个业务服务。
- Gateway做统一入口认证、粗授权、Header清洗、限流、审计。
- 每个服务都作为Resource Server校验Token。
- Controller或方法注解做接口权限。
- Service层做数据权限和字段权限。
- 用户调用用用户Token,系统调用用
client_credentials服务账号Token。 - JWT短有效期,不放敏感信息。
- 生产环境配合mTLS、网络策略和最小权限。
- 所有拒绝都区分401、403、429、503。
- 审计导出、删除、支付、退款、医疗数据访问等高风险操作。
十六、面试标准回答
如果面试官问“Gateway怎么做统一认证,OAuth2怎么接,各服务怎么授权”,可以这样答:
我们一般把认证授权中心独立出来,比如使用Keycloak、Spring Authorization Server
或公司IAM。用户登录走OAuth2/OIDC授权码模式,认证中心签发Access Token。
Gateway作为统一入口,可以作为Resource Server校验JWT,也可以作为OAuth2 Client
完成登录并通过TokenRelay把Access Token转发给下游。Gateway负责统一认证、
路由级粗授权、Header清洗、限流、审计和Trace,不承载复杂业务权限。
下游每个服务也要作为Resource Server校验Token,不能只相信Gateway传来的
X-User-Id。接口权限可以用URL规则或@PreAuthorize判断scope和权限码;
数据权限必须在Service层结合租户、组织、资源归属继续校验。服务间调用如果代表用户,
通过Feign透传用户Token;如果是定时任务或系统任务,用client_credentials获取服务账号Token。
生产上要校验iss、aud、exp、kid,做好JWT短有效期、密钥轮换、Token撤销、
401/403区分、Header清洗、mTLS或网络隔离以及审计日志。关联学习
- Gateway基础与过滤链
- Gateway内部链路与生产治理
- Spring Security核心架构
- Spring Security JWT认证
- Spring Security授权模型
- 信息安全:JWT与接口签名
