Skip to content

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。

二、推荐总体架构

mermaid
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。

mermaid
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,请求时携带:

text
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...

Gateway校验后再转发给下游,下游服务也校验Token。

mermaid
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。

mermaid
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常见依赖:

xml
<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选择兼容版本,并注意javaxjakarta的差异。不要把Boot 3配置直接复制到Boot 2老项目。

5.2 Gateway配置

yaml
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://auth.example.com/realms/medical

issuer-uri会让应用读取授权服务器的OIDC元数据和JWK公钥地址。生产上要保证授权中心高可用,并设置合理缓存;密钥轮换时新旧Key要有重叠窗口。

5.3 WebFlux安全配置

Gateway基于WebFlux,使用SecurityWebFilterChain

java
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();
    }
}

这段配置做了三件事:

  1. 健康检查和公开接口放行。
  2. 管理接口要求admin scope。
  3. 其他请求必须携带有效JWT。

注意:Gateway这里做的是入口粗授权,不是完整业务授权。比如order:read只能说明用户可访问订单查询接口,不能说明他能看所有医院、所有租户、所有订单。

六、Gateway作为OAuth2 Client登录并TokenRelay

适合BFF模式。Gateway负责发起OAuth2登录,登录成功后把Access Token转发给下游。

6.1 配置OAuth2 Client

yaml
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/medical

6.2 开启oauth2Login

java
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

yaml
spring:
  cloud:
    gateway:
      routes:
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/api/orders/**
          filters:
            - TokenRelay=
            - StripPrefix=1

TokenRelay会把当前登录用户关联的Access Token放到转发请求中。下游服务仍按资源服务器方式验Token。

七、业务服务怎样认证和授权

每个业务服务建议也配置成Resource Server。这样即使绕过Gateway访问服务,只要没有合法Token,也会被拒绝。

7.1 Servlet服务JWT校验

yaml
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://auth.example.com/realms/medical
java
import 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 方法级授权

java
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层做

java
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里建议只放稳定、必要、低敏感声明:

json
{
  "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。

常见权限分层:

权限层示例判断位置
登录态是否有效TokenGateway和各服务
路由权限能否访问/api/admin/**Gateway
接口权限order:readorder:refundController或方法授权
数据权限医院、科室、租户、资源归属Service和SQL条件
字段权限是否能看身份证、手机号DTO组装、脱敏组件
操作审计谁导出了患者数据审计日志和风控

九、Gateway向下游传什么Header

不要把外部请求头原样透传给下游。Gateway应先清洗,再覆盖内部可信Header。

推荐:

Header来源用途
Authorization原始Access Token或TokenRelay下游资源服务器验Token
X-Trace-IdGateway生成或沿用链路追踪
X-User-IdGateway验Token后覆盖写入便捷审计,不能单独作为认证依据
X-Tenant-IdGateway验Token后覆盖写入租户路由和日志
X-Request-IdGateway生成幂等、排查

危险做法:

text
客户端传 X-User-Id: 1
Gateway原样转发
下游相信 X-User-Id

正确做法是:下游以JWT验签结果为准,Header只作为辅助上下文;如果确实要信任Gateway写入的内部Header,服务间网络必须隔离,并且Gateway要删除外部同名Header后重新写。

java
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。

mermaid
flowchart TD
    A["用户请求订单服务"] --> B["订单服务读取当前JWT"]
    B --> C["Feign RequestInterceptor透传Authorization"]
    C --> D["库存服务验JWT和权限"]

Feign拦截器示例:

java
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,而不是伪造某个管理员用户。

mermaid
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生产建议:

  1. 使用非对称签名,例如RS256或ES256。
  2. 校验issaudexpnbf
  3. Access Token短有效期。
  4. Refresh Token只保存在可信客户端或服务端。
  5. 支持Key Rotation。
  6. 敏感数据不进payload。
  7. 权限变化用token_version、黑名单或短Token窗口控制。

11.1 JWK公钥缓存和密钥轮换

JWT验签通常不是每次请求都远程调用认证中心。资源服务器会从issuer-uri发现OIDC元数据,再拿到jwks_uri,下载一组JWK公钥并缓存在本地。每次请求到来时,根据JWT Header里的kid在本地JWK缓存中找公钥验签。

mermaid
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。生产上要把kidissueraudience、JWK刷新时间和验签失败原因打到安全日志里,否则只能看到“用户突然登录失效”。

排查401时特别关注:

证据说明
JWT Header里的kid判断使用哪把钥匙签发
资源服务器JWK缓存更新时间判断是否拿到了新公钥
认证中心当前JWK集合判断新旧公钥是否同时发布
Token的issaudissuer或audience错也会验签后被拒
网关和下游服务配置Gateway能过但下游401,常是issuer/audience/JWK缓存不同

11.2 Token撤销和权限变更为什么不一定实时生效

JWT是自包含Token。资源服务器本地验签通过后,就能读取其中的用户、scope、租户和过期时间。它的好处是高性能,坏处是撤销不天然实时。

mermaid
flowchart TD
    A["管理员收回用户权限"] --> B["权限库已更新"]
    B --> C["用户旧JWT仍未过期"]
    C --> D["资源服务器本地验签通过"]
    D --> E["Token中旧scope仍可被读取"]
    E --> F["可能继续访问旧权限"]

解决方案不是单一开关,而是按安全等级组合:

方案原理优点代价
短Access Token让旧权限最多存活几分钟简单、压力小无法秒级撤销
Refresh Token服务端受控续期时重新加载权限权限最终收敛需要会话和刷新流程
token_versionToken带版本,资源服务器校验版本是否仍有效改密、禁用、收权可立即失效每次或缓存查询版本
黑名单/撤销表记录被撤销的jti或Token摘要针对单Token撤销高并发要缓存和过期清理
Opaque Token introspection每次向认证中心或缓存查询Token状态撤销强一致性更好认证中心成为在线依赖
关键权限实时查Token只放身份,权限从缓存或库读取权限变更可控多一次缓存/DB访问

商业系统通常这样折中:

  1. Access Token设置短有效期,例如5到30分钟,具体看风险。
  2. Refresh Token只由可信客户端或Gateway BFF保存。
  3. JWT里放subtenant_id、少量稳定scope和token_version,不放敏感数据。
  4. 高风险接口,例如导出医疗数据、退款、删除资产、修改权限,服务端实时查权限缓存和数据权限。
  5. 用户禁用、改密码、权限收回时,递增token_version并删除权限缓存。
  6. 所有服务校验Token版本时使用本地短缓存,避免每次请求都打数据库。

JDK 8 Demo:用tokenVersion让旧Token失效。

java
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尽快失效。

十二、各服务到底要不要再次认证

建议要。

mermaid
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过期和无权限都返回同一个“系统错误”。前端、运维和安全审计需要明确区分。

十四、生产排查流程

mermaid
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排查

  1. 请求是否携带Authorization: Bearer xxx
  2. Token是否过期。
  3. JWT的iss是否和配置的issuer-uri一致。
  4. JWT的aud是否包含当前API。
  5. JWT Header的kid是否能在JWK集合中找到。
  6. 授权中心公钥是否轮换但服务未刷新。
  7. Gateway是否把Authorization转发给下游。
  8. 代理或Nginx是否删除了Authorization Header。

14.2 403排查

  1. 当前用户是否认证成功。
  2. JWT里是否有需要的scope或权限。
  3. Spring Security权限前缀是否匹配,例如SCOPE_order:read
  4. 方法上的@PreAuthorize表达式是否正确。
  5. 数据权限是否拒绝,例如租户、医院、科室不匹配。
  6. 权限刚变更但旧Token仍未过期。

14.3 Gateway认证通过但下游401

常见原因:

原因说明
Gateway没有透传Authorization下游拿不到Token
TokenRelay未生效BFF模式下授权客户端信息没有关联
下游issuer配置不同Gateway和服务校验的认证中心不一致
下游audience更严格Token不是发给该资源服务的
Header被Strip或重写路由Filter或代理清掉了Header

十五、商业落地建议

  1. 认证中心独立建设,不把登录逻辑散落到每个业务服务。
  2. Gateway做统一入口认证、粗授权、Header清洗、限流、审计。
  3. 每个服务都作为Resource Server校验Token。
  4. Controller或方法注解做接口权限。
  5. Service层做数据权限和字段权限。
  6. 用户调用用用户Token,系统调用用client_credentials服务账号Token。
  7. JWT短有效期,不放敏感信息。
  8. 生产环境配合mTLS、网络策略和最小权限。
  9. 所有拒绝都区分401、403、429、503。
  10. 审计导出、删除、支付、退款、医疗数据访问等高风险操作。

十六、面试标准回答

如果面试官问“Gateway怎么做统一认证,OAuth2怎么接,各服务怎么授权”,可以这样答:

text
我们一般把认证授权中心独立出来,比如使用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或网络隔离以及审计日志。

关联学习

参考资料