SpringCloud Gateway + OAuth2.0 + JWT 微服务认证授权实战
简介这份资源是一套基于SpringCloud Gateway整合OAuth2.0与JWT实现微服务统一认证授权的完整源码面向具备一定SpringCloud基础、正在搭建微服务安全体系的Java后端开发者可解决网关层鉴权、令牌签发与校验、用户信息传递等典型问题。压缩包共270个文件约340KB以204个xml配置、56个java源码为主辅以yml、sql、png等涵盖网关全局过滤器、认证服务配置、令牌服务实现、Redis工具类及自定义用户信息转换器等核心模块需自行调整Redis与JDBC连接并安装Nacos后即可测试认证服务、资源服务与用户信息接口。目前已有7162人学习下载读者可借此理清OAuth2授权码流程与JWT令牌在网关中的落地方式掌握过滤器链、安全配置与Redis缓存令牌的排错思路快速搭建可运行的微服务认证骨架。1. 网关做认证授权为什么比每个微服务自己扛更靠谱一个典型的微服务现场订单、库存、用户三个服务各自写了一套 JWT 解析逻辑某天密钥轮换三个服务上线时间差了半小时结果半小时内部分请求 401、部分 200排查到凌晨。这类事故的根因不是 JWT 本身而是认证逻辑散落在每个服务里。SpringCloud Gateway 加 OAuth2.0 加 JWT 的组合核心思路就是把「你是谁、你能干什么」这两件事从业务服务里抽出来统一压到网关这一层。网关校验令牌、解析权限、把用户身份透传给下游下游服务只信任来自网关的请求。这套方案适合已经用 SpringCloud 体系、服务数量超过三个、且不想在每个服务里重复写安全代码的团队。下面按「令牌怎么走、网关怎么配、坑在哪」的顺序拆开讲。2. 令牌从哪来、网关怎么验OAuth2.0 与 JWT 的职责边界2.1 先分清认证服务器和资源服务器的角色OAuth2.0 定义的是授权框架不是认证协议本身。在这套架构里认证服务器负责颁发令牌网关扮演资源服务器Resource Server的角色负责校验令牌。JWT 是令牌的载体格式它把用户身份和权限声明claims编码进一个自包含的字符串里网关不需要每次请求都去认证服务器查库本地验签即可。常见做法是认证服务器用授权码模式authorization_code给前端发 access_token前端把 token 放在 Authorization 头里请求网关。网关验签通过后从 claims 里取出用户 ID 和权限列表转成下游能识别的请求头。这里有个关键决策网关是只做验签还是同时做权限判断我的建议是验签和粗粒度权限判断都在网关做细粒度的数据级权限留给业务服务否则网关会变成逻辑黑洞。JWT 的结构是三段式header.payload.signature。header 声明算法payload 放 claimssignature 是对前两段的签名。网关验签用的是认证服务器公钥非对称算法 RS256或共享密钥对称算法 HS256。生产环境优先 RS256因为认证服务器只需要暴露公钥私钥不出域。2.2 网关侧依赖与最小配置先看依赖。SpringCloud Gateway 本身是响应式的基于 WebFlux所以安全模块要用 spring-boot-starter-oauth2-resource-server不能用传统的 spring-security-oauth2。这一点踩过的人不少引错了包会导致过滤器链根本不生效。!-- pom.xml 关键依赖版本跟随 Spring Boot 父工程管理 -- dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-gateway/artifactId /dependency dependency !-- 响应式资源服务器注意不是 spring-security-oauth2-resource-server -- groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-oauth2-resource-server/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency依赖说明gateway 提供路由和过滤器能力oauth2-resource-server 提供 JWT 解码与验签security 提供过滤器链。三者缺一不可且必须都是响应式版本。接下来是 application.yml 里的核心配置。jwk-set-uri 指向认证服务器暴露公钥的端点网关启动时会去拉取 JWK Set 并缓存验签时用对应的公钥。spring: security: oauth2: resourceserver: jwt: # 认证服务器公钥端点网关据此验签 jwk-set-uri: http://auth-server:9000/oauth2/jwks # 校验签发者防止别的认证服务器签的令牌混进来 issuer-uri: http://auth-server:9000 cloud: gateway: routes: - id: order-service uri: lb://order-service predicates: - Path/api/order/** filters: # 去掉前缀再转发给下游 - StripPrefix1参数说明jwk-set-uri 是验签公钥来源issuer-uri 用于校验 iss 声明两者建议都配。StripPrefix 按路径层级设置配错会导致下游 404。lb:// 表示走服务发现负载均衡需要注册中心支持。2.3 自定义过滤器把用户身份透传给下游网关验签通过后SecurityContext 里有了 JwtAuthenticationToken。但下游服务拿不到这个上下文需要网关主动把用户信息写进请求头。这里用全局过滤器实现。Component public class AuthHeaderFilter implements GlobalFilter, Ordered { Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { return exchange.getPrincipal() .cast(JwtAuthenticationToken.class) .flatMap(auth - { Jwt jwt auth.getToken(); // 从 claims 取用户标识和权限写入下游请求头 ServerHttpRequest mutated exchange.getRequest().mutate() .header(X-User-Id, jwt.getSubject()) .header(X-User-Roles, String.join(,, jwt.getClaimAsStringList(roles))) .build(); return chain.filter(exchange.mutate().request(mutated).build()); }) // 无认证信息时直接放行由安全链决定是否拦截 .switchIfEmpty(chain.filter(exchange)); } Override public int getOrder() { // 数值越小优先级越高需在安全过滤器之后执行 return -100; } }逻辑说明exchange.getPrincipal() 拿到当前认证主体强转为 JwtAuthenticationToken 后取出 Jwt 对象。getSubject() 是用户唯一标识getClaimAsStringList(roles) 取权限列表。mutate() 构造新的请求对象把身份写进 X-User-Id 和 X-User-Roles 头。switchIfEmpty 处理匿名请求交给后续安全链判断。getOrder 返回 -100确保在安全过滤器之后、路由转发之前执行。注意下游服务必须校验请求来源只信任网关转发的请求头。如果下游直接暴露公网攻击者可以伪造 X-User-Id 头绕过认证。常见做法是网关和下游走内网或加一个内部签名头。3. 认证服务器怎么发令牌从授权码到 JWT 签发3.1 授权码模式的最小落地步骤认证服务器可以用 Spring Authorization Server 搭建。核心是注册一个 RegisteredClient配置授权类型、回调地址、令牌有效期。下面是一个内存注册的示例生产环境应改为数据库存储。Configuration public class AuthServerConfig { Bean public RegisteredClientRepository registeredClientRepository() { RegisteredClient client RegisteredClient.withId(gateway-client) .clientId(gateway-client) // 生产环境用 BCrypt 加密存储 .clientSecret({noop}secret) .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) // 授权码 刷新令牌 .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) .redirectUri(http://localhost:8080/login/oauth2/code/gateway) .scope(read) .scope(write) // access_token 有效期 30 分钟 .tokenSettings(TokenSettings.builder() .accessTokenTimeToLive(Duration.ofMinutes(30)) .refreshTokenTimeToLive(Duration.ofDays(7)) .build()) .build(); return new InMemoryRegisteredClientRepository(client); } }参数说明clientId 和 clientSecret 是客户端凭证redirectUri 必须和前端回调地址完全一致差一个斜杠都会报错。accessTokenTimeToLive 建议 15 到 30 分钟太长会增加泄露风险太短会导致频繁刷新。refreshTokenTimeToLive 可以设长一些但要有吊销机制。3.2 JWT 里该放哪些 claimsclaims 的设计直接影响网关和下游能拿到什么信息。标准 claims 包括 sub用户标识、iss签发者、exp过期时间、iat签发时间。自定义 claims 建议放 roles 和 tenantId 这类网关需要判断的字段。Bean public OAuth2TokenCustomizerJwtEncodingContext jwtCustomizer() { return context - { if (context.getTokenType().equals(OAuth2TokenType.ACCESS_TOKEN)) { Authentication principal context.getPrincipal(); // 把用户权限写进 roles claim context.getClaims().claim(roles, principal.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toList())); // 多租户场景下带上租户标识 context.getClaims().claim(tenantId, resolveTenant(principal)); } }; }逻辑说明OAuth2TokenCustomizer 在令牌签发前被调用可以往 claims 里塞自定义字段。roles 从 Authentication 的权限列表转换而来tenantId 从用户上下文解析。注意不要往 JWT 里放敏感信息JWT 只是 Base64 编码不是加密任何人都能解码看到 payload。注意JWT 的 payload 是明文可读的不要放手机号、身份证号这类敏感数据。需要传递敏感信息时放一个引用 ID让下游拿 ID 去查。3.3 令牌校验的两种模式对比网关校验令牌有两种常见模式本地验签和远程 introspection。本地验签用公钥直接验证签名性能好但令牌吊销后无法实时感知。远程 introspection 每次请求都问认证服务器实时性好但认证服务器会成为瓶颈。对比项本地验签JWT远程 introspection性能高无网络开销低每次请求多一跳实时吊销不支持需等过期支持认证服务器压力仅启动时拉公钥每请求一次适用场景令牌短有效期、可容忍延迟吊销高安全要求、需即时吊销我的选择是本地验签为主access_token 有效期设短15 分钟配合 refresh_token 刷新。需要即时吊销的场景维护一个短期的黑名单缓存网关验签后再查一次黑名单。这样兼顾性能和可控性。4. 避坑与排查网关认证最容易翻车的五个点4.1 引错依赖导致过滤器链不生效现象配置写好了但请求不带 token 也能访问受保护接口安全配置像没生效一样。原因SpringCloud Gateway 基于 WebFlux如果引入了 spring-boot-starter-web 或传统的 spring-security-oauth2会触发 Servlet 容器自动配置和 WebFlux 冲突安全过滤器链根本不注册。解决检查依赖树排除 spring-boot-starter-web确保用的是 spring-boot-starter-oauth2-resource-server。用 mvn dependency:tree 排查看到 spring-webmvc 就要警惕。4.2 jwk-set-uri 配错导致启动报错或验签失败现象网关启动时报 Unable to resolve the JWK Set或运行时报签名验证失败。原因jwk-set-uri 地址写错、认证服务器未启动、或网络不通。另一个隐蔽原因是 issuer-uri 和 jwk-set-uri 指向的认证服务器不一致导致 iss 校验失败。解决先用 curl 直接访问 jwk-set-uri确认返回 JSON 格式的 keys 数组。再检查 issuer-uri 是否和令牌里的 iss 完全一致包括端口和路径。4.3 请求头透传被下游忽略现象网关日志显示 X-User-Id 已写入但下游服务读不到。原因下游服务用了 SpringMVC默认不会把自定义头注入到 Controller 参数里需要显式用 RequestHeader 读取。另一个原因是网关的 StripPrefix 配置和下游路径不匹配请求根本没到目标接口。解决下游 Controller 显式声明 RequestHeader(X-User-Id) String userId。检查网关路由的 predicates 和 filters用 actuator 的 gateway/routes 端点确认路由规则。4.4 时钟偏移导致令牌误判过期现象令牌刚签发就报过期或明明过期了还能用。原因网关和认证服务器所在机器的系统时钟不同步JWT 的 exp 和 iat 校验依赖系统时间。解决所有节点接入同一 NTP 源偏差控制在秒级以内。网关侧可以配置 JwtDecoder 的时钟偏移容忍度但这是补救不是根治。Bean public JwtDecoder jwtDecoder() { NimbusJwtDecoder decoder NimbusJwtDecoder .withJwkSetUri(http://auth-server:9000/oauth2/jwks) .build(); // 允许 60 秒时钟偏移缓解不同步问题 decoder.setJwtValidator(new DelegatingOAuth2TokenValidator( JwtValidators.createDefaultWithIssuer(http://auth-server:9000), new JwtTimestampValidator(Duration.ofSeconds(60)) )); return decoder; }参数说明JwtTimestampValidator 的构造参数是容忍的偏移量设 60 秒意味着 exp 过期后 60 秒内仍接受。这个值不宜过大否则失去过期保护的意义。4.5 跨域配置和认证过滤器顺序冲突现象前端预检请求OPTIONS返回 401导致跨域失败。原因CORS 预检请求不带 Authorization 头被安全过滤器拦截。解决在安全配置里放行 OPTIONS 请求并确保 CORS 配置在安全过滤器之前生效。常见做法是在 SecurityWebFilterChain 里加 cors() 并配置 CorsConfigurationSource同时对 OPTIONS 请求 permitAll。Bean public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { return http .csrf(ServerHttpSecurity.CsrfSpec::disable) .cors(cors - cors.configurationSource(corsConfigurationSource())) .authorizeExchange(ex - ex // 预检请求放行 .pathMatchers(HttpMethod.OPTIONS).permitAll() .pathMatchers(/api/public/**).permitAll() .anyExchange().authenticated()) .oauth2ResourceServer(oauth2 - oauth2.jwt(Customizer.withDefaults())) .build(); }逻辑说明csrf 关闭是因为纯 token 认证不依赖 Cookie。cors 指定配置源。pathMatchers 放行 OPTIONS 和公开接口其余全部要求认证。oauth2ResourceServer 启用 JWT 校验。顺序上 cors 要在 authorizeExchange 之前配置。5. 进阶令牌吊销、权限细粒度与灰度验证令牌吊销是 JWT 方案的天然短板。JWT 自包含签发后认证服务器管不着。要支持吊销常见做法是维护一个吊销列表网关验签后查一次。列表可以放 Rediskey 是 jtiJWT IDvalue 是过期时间设置 TTL 和令牌剩余有效期一致到期自动清理。Component public class TokenRevocationFilter implements GlobalFilter, Ordered { private final ReactiveStringRedisTemplate redisTemplate; public TokenRevocationFilter(ReactiveStringRedisTemplate redisTemplate) { this.redisTemplate redisTemplate; } Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { return exchange.getPrincipal() .cast(JwtAuthenticationToken.class) .flatMap(auth - { String jti auth.getToken().getId(); // 查 Redis 黑名单命中则拒绝 return redisTemplate.hasKey(revoked: jti) .flatMap(revoked - { if (Boolean.TRUE.equals(revoked)) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } return chain.filter(exchange); }); }) .switchIfEmpty(chain.filter(exchange)); } Override public int getOrder() { // 在 AuthHeaderFilter 之前执行 return -200; } }逻辑说明从 Jwt 取 jti查 Redis 里是否有 revoked: 前缀的 key。命中则返回 401 并结束响应。getOrder 返回 -200比 AuthHeaderFilter 的 -100 更早执行确保被吊销的令牌不会进入后续流程。Redis 的 TTL 设置为令牌剩余有效期避免黑名单无限增长。权限细粒度方面网关适合做接口级权限判断比如 /api/admin/** 需要 ROLE_ADMIN。数据级权限比如「只能查自己部门的订单」必须留给业务服务因为网关拿不到业务数据。我的习惯是在网关配置里维护一张路径到角色的映射表用配置文件驱动避免硬编码。灰度验证是上线前的后悔药。新认证逻辑不要一次性全量切先按用户 ID 哈希放量 5%观察网关的错误率和认证服务器压力。具体做法是在网关加一个灰度过滤器根据用户 ID 取模决定走新逻辑还是旧逻辑。验证指标看三个401 比例、令牌验签耗时 P99、认证服务器 QPS。401 比例突增说明验签或权限判断有问题验签耗时上涨说明公钥缓存没生效QPS 飙升说明本地验签退化成了远程调用。最后说一个我踩过的坑早期为了省事把 roles 直接拼成字符串放请求头下游用逗号分割。结果某个角色名里带了逗号权限判断直接错乱。后来改成 Base64 编码再传下游解码后按 JSON 解析。这个细节不起眼但权限错乱比认证失败更危险因为它不会报错只会静默放行。做认证授权宁可多一层编码也不要让格式歧义留在链路上。希望帮到你。本文还有配套的精品资源点击获取