
简介这份资源是面向 Java 后端与微服务开发者的 SpringCloud Gateway OAuth2.0 JWT 认证授权实战源码适合正在搭建统一鉴权体系、需要打通网关层与资源服务权限校验的中高级开发者参考。项目以网关作为统一入口结合 OAuth2 授权服务器与 JWT 令牌完成用户身份认证、令牌签发与资源访问控制并借助 Redis 存储令牌、JDBC 持久化客户端与用户信息同时依赖 Nacos 做服务注册与配置管理覆盖认证服务、资源服务与用户信息查询等典型调用链路。压缩包共 270 个文件以 204 个 xml 配置、56 个 java 源码为主另含 yml、sql 脚本与少量图片整体约 340KB结构紧凑便于快速导入 IDE 阅读。目前已有 7162 人学习下载读者可据此理解过滤器、令牌服务、授权服务器配置与安全配置等核心模块的协作方式并在此基础上改造出符合自身业务的安全方案。1. 网关做认证这件事为什么我劝你别再每个服务写一遍去年帮一个团队排查线上问题六个微服务里五个都在自己的application.yml里配了同一套 JWT 校验逻辑结果其中一个服务因为依赖版本差了一个小版本解析 token 时对exp字段的处理不一致导致用户在 A 服务能正常访问、跳到 B 服务就被踢回登录页。这种每个服务各写一遍认证的架构在微服务数量超过三个之后基本就是灾难——密钥散落、逻辑漂移、改一处要发六个包。springcloud-gateway-oauth2这份源码包解决的正是这个痛点把 OAuth2.0 的令牌签发、JWT 的解析校验、以及基于角色的权限判断全部收敛到 SpringCloud Gateway 这一层。网关作为所有流量的唯一入口在请求转发到下游微服务之前就完成身份认证和授权决策下游服务只需要信任网关透传的用户信息不再关心 token 怎么来的、怎么验的。这套方案适合三类人正在做微服务拆分、被认证逻辑重复困扰的后端需要给现有系统加统一登录入口的架构同学以及想搞明白 OAuth2 密码模式、授权码模式在网关层到底怎么落地的一线开发。源码包里包含完整的网关配置、资源服务器配置、JWT 工具类和几个模拟业务服务拿来就能跑通登录拿 token → 带 token 访问 → 网关校验放行这条链路。下面我按实际拆包的顺序把关键配置和几个容易翻车的地方讲清楚。2. 拆开源码包网关、认证中心、资源服务到底怎么分工2.1 三个角色的职责边界这份源码的结构是典型的三角色模型理解它们各自管什么后面配参数才不会乱。网关gateway是唯一对外暴露的入口端口通常是 8080 或 9000。它承担两件事一是路由转发把/api/user/**转到用户服务、/api/order/**转到订单服务二是全局过滤器里的 token 校验请求进来先过GlobalFiltertoken 不合法直接返回 401根本不会打到下游。认证中心auth-server负责签发 token。用户拿用户名密码来换它验证通过后用 JWT 格式生成 access_token 返回。这个模块依赖 Spring Security OAuth2 的授权服务器能力密码模式下核心是TokenEndpoint那套流程。资源服务resource-server是真正干活的业务微服务。它们不直接面向公网只接受网关转发过来的请求。关键点是资源服务不再自己解析 token而是从请求头里读网关塞进去的用户信息比如X-User-Id、X-User-Roles。提示如果你的资源服务还能被外网直接访问那网关的认证就是纸糊的。生产环境务必让资源服务只监听内网地址或者用安全组限制来源。2.2 认证中心的核心配置先看认证中心怎么签发 JWT。源码里AuthorizationServerConfig这个类是整个签发流程的入口我把它精简成能直接抄的版本Configuration EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { Autowired private AuthenticationManager authenticationManager; Autowired private JwtAccessTokenConverter jwtAccessTokenConverter; // 配置 token 的存储方式和签名 Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) { endpoints .authenticationManager(authenticationManager) // 密码模式必须显式注入 .accessTokenConverter(jwtAccessTokenConverter) // 用 JWT 而非默认的 UUID token .allowedTokenEndpointRequestMethods(HttpMethod.POST); } // 配置客户端信息谁有资格来申请 token Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient(gateway-client) // 客户端 id .secret(passwordEncoder().encode(secret-123)) // 客户端密钥必须加密存储 .authorizedGrantTypes(password, refresh_token) .scopes(all) .accessTokenValiditySeconds(7200) // access_token 有效期 2 小时 .refreshTokenValiditySeconds(604800); // refresh_token 有效期 7 天 } }逻辑说明configure(AuthorizationServerEndpointsConfigurer)决定 token 怎么生成这里注入authenticationManager是因为密码模式需要它去校验用户名密码注入jwtAccessTokenConverter是把默认的随机字符串 token 换成自包含的 JWT。configure(ClientDetailsServiceConfigurer)决定谁能来申请 tokenwithClient和secret是客户端凭证authorizedGrantTypes里password支持用户名密码直接换 tokenrefresh_token支持过期后用刷新令牌续期。参数怎么改accessTokenValiditySeconds设太短用户频繁掉线设太长泄露风险大一般业务系统 2 小时是平衡点refreshTokenValiditySeconds可以给到 7 天甚至 30 天因为它只在换 token 时用一次。secret千万别用明文源码里用了passwordEncoder().encode()你换成自己的 BCrypt 加密串。2.3 JWT 签名密钥的配置JWT 的安全性全压在签名密钥上。源码里用对称加密HMAC做签名配置在JwtAccessTokenConverter的 bean 里Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter converter new JwtAccessTokenConverter(); converter.setSigningKey(your-256-bit-secret-key-here-must-be-long); // 签名密钥 // 如果要在 token 里塞自定义信息比如用户 id converter.setAccessTokenConverter(new CustomAccessTokenConverter()); return converter; }逻辑说明setSigningKey设置的是 HMAC 的密钥认证中心用它签名网关用它验签两边必须完全一致。CustomAccessTokenConverter是自定义转换器用来往 JWT 的 payload 里加额外字段比如用户 id、角色列表。参数说明密钥长度建议 256 位以上32 个字符以上太短容易被暴力破解。对称加密的优点是配置简单、验签快缺点是认证中心和网关都得持有同一个密钥一旦泄露全线沦陷。如果对安全要求更高可以换成非对称加密RS256认证中心持私钥签名、网关持公钥验签这样网关即使被攻破也拿不到签名能力。源码默认给的是对称方案够用但不是最安全的。3. 网关层的 token 校验过滤器怎么写才不漏3.1 全局过滤器的执行时机网关校验 token 靠的是GlobalFilter。这里有个新手最容易踩的点过滤器有order值值越小越先执行。认证过滤器必须排在路由转发之前否则请求已经打到下游了才校验等于没校验。Component public class AuthGlobalFilter implements GlobalFilter, Ordered { Autowired private JwtUtil jwtUtil; // 白名单登录接口、健康检查等不需要 token private static final ListString WHITE_LIST Arrays.asList( /auth/login, /auth/refresh, /actuator/health ); Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { String path exchange.getRequest().getURI().getPath(); // 白名单直接放行 if (WHITE_LIST.stream().anyMatch(path::startsWith)) { return chain.filter(exchange); } // 从 Authorization 头里取 token String authHeader exchange.getRequest().getHeaders().getFirst(Authorization); if (authHeader null || !authHeader.startsWith(Bearer )) { return unauthorized(exchange, 缺少 token); } String token authHeader.substring(7); // 去掉 Bearer 前缀 try { Claims claims jwtUtil.parseToken(token); // 解析并验签 // 把用户信息塞进请求头透传给下游 ServerHttpRequest mutated exchange.getRequest().mutate() .header(X-User-Id, claims.getSubject()) .header(X-User-Roles, String.valueOf(claims.get(roles))) .build(); return chain.filter(exchange.mutate().request(mutated).build()); } catch (Exception e) { return unauthorized(exchange, token 无效或已过期); } } Override public int getOrder() { return -100; // 负数确保在路由转发过滤器之前执行 } private MonoVoid unauthorized(ServerWebExchange exchange, String msg) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); exchange.getResponse().getHeaders().add(Content-Type, application/json;charsetUTF-8); byte[] bytes ({\code\:401,\msg\:\ msg \}).getBytes(StandardCharsets.UTF_8); return exchange.getResponse().writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(bytes))); } }逻辑说明filter方法先判断路径是否在白名单是就直接放行否则从Authorization头取 token格式必须是Bearer xxx解析成功后把用户 id 和角色写进新的请求头用mutate()构造新请求再转发这样下游服务就能直接读X-User-Id。getOrder返回 -100保证认证在路由之前。参数说明WHITE_LIST里放不需要认证的路径登录、刷新 token、健康检查是标配。X-User-Id和X-User-Roles这两个头名要和下游服务约定好别一边叫X-User-Id一边叫userId。unauthorized方法返回的是 JSON 而不是默认的 HTML 错误页前端处理起来更省事。3.2 路由配置与下游透传网关的路由配置决定了请求往哪转。源码里用的是 YAML 配置我把它整理成表格方便对照配置项作用示例值id路由唯一标识user-serviceuri下游服务地址lb://user-servicepredicates匹配条件Path/api/user/**filters路由级过滤器StripPrefix1lb://前缀表示走服务发现负载均衡需要配合注册中心使用。StripPrefix1表示转发时去掉路径的第一段比如/api/user/info转发到下游变成/user/info这个参数配错了会导致下游 404是高频翻车点。注意网关透传的X-User-Id这类头如果外部请求里也带了同名头会被恶意伪造。正确做法是在网关入口处先剥离所有X-开头的头再重新写入。源码里如果没做这一步记得自己补上。3.3 下游资源服务怎么读用户信息下游服务不再解析 token而是从请求头读。一个典型的 Controller 长这样RestController RequestMapping(/user) public class UserController { GetMapping(/info) public ResultUserVO info(RequestHeader(X-User-Id) String userId, RequestHeader(value X-User-Roles, required false) String roles) { // 直接用网关透传的用户 id 查数据不再碰 token UserVO vo userService.getById(userId); return Result.success(vo); } }逻辑说明RequestHeader直接读网关塞进来的头required false是因为有些接口可能允许匿名访问角色头可能为空。这样下游服务完全解耦了认证逻辑换认证方案时只改网关下游一行不动。参数说明如果下游服务也想做细粒度权限控制比如只有管理员能调某个接口可以在方法上加自定义注解配合拦截器读X-User-Roles判断。但记住核心的你是谁由网关保证你能不能干这个可以在下游做也可以统一在网关做看团队习惯。4. 避坑排查这五个地方我踩过你别再踩4.1 现象网关放行了但下游一直 401原因下游服务自己也引了 Spring Security 依赖默认开启了认证网关虽然透传了用户信息但下游的 Security 过滤器链不认识直接拦截。解决下游服务的SecurityConfig里要放行所有请求或者干脆不引 Security 依赖。配置成http.authorizeRequests().anyRequest().permitAll()把认证职责完全交给网关。这个坑我见过至少三个团队踩排查半天以为是 token 问题其实是下游自己又拦了一道。4.2 现象token 明明没过期网关却报验签失败原因认证中心和网关的签名密钥不一致。常见于配置从 YAML 挪到配置中心后一边读到了新值一边还是旧值或者密钥里有特殊字符YAML 解析时被转义了。解决把密钥抽成环境变量或配置中心统一管理两边引用同一个 key。排查时可以在网关打印一下实际用的密钥长度和认证中心对比。密钥里尽量只用字母数字避免$、#这类在 YAML 里有特殊含义的字符。4.3 现象跨域请求预检OPTIONS被网关拦截返回 401原因浏览器的 CORS 预检请求不带Authorization头网关的认证过滤器把它当未认证请求拦了。解决在WHITE_LIST里放行OPTIONS方法或者在过滤器里判断exchange.getRequest().getMethod() HttpMethod.OPTIONS时直接放行。同时网关要配好 CORS 相关响应头否则预检过了正式请求还是会被浏览器拦。4.4 现象刷新 token 后旧 token 还能用原因JWT 是无状态的签发后服务端不保存所以没法主动作废。用户刷新拿到新 token旧 token 在过期前依然有效。解决这是 JWT 的固有特性不是 bug。如果业务要求旧 token 立即失效得引入 Redis 黑名单签发时把 token 的 jti 存进 Redis 并设过期时间网关校验时查一下黑名单。代价是每次请求多一次 Redis 查询牺牲了 JWT 无状态的优势。源码默认没做黑名单按需自己加。4.5 现象网关转发后下游拿到的用户 id 是乱码原因请求头里塞了中文或特殊字符HTTP 头默认用 ISO-8859-1 编码中文会乱。解决用户 id 用数字或 UUID别用中文。角色信息如果非要用中文先 URL 编码再塞进头里下游解码。更稳妥的做法是头里只传 id角色信息让下游拿 id 去缓存或数据库查。5. 进阶把权限判断也搬到网关以及怎么验证整套链路5.1 网关层做基于角色的路由级鉴权基础版只校验 token 有效性进阶版可以在网关直接判断角色。比如订单管理接口只允许ROLE_ADMIN访问在过滤器里加一段// 在 token 解析成功后根据路径和角色做判断 String roles String.valueOf(claims.get(roles)); if (path.startsWith(/api/order/admin) !roles.contains(ROLE_ADMIN)) { return forbidden(exchange, 权限不足); }逻辑说明claims.get(roles)拿到的是签发时塞进 JWT 的角色列表contains判断是否包含所需角色。forbidden方法返回 403。这样权限规则集中在网关一处改规则不用动下游。参数说明角色字符串的格式要和签发端约定好是逗号分隔的ROLE_ADMIN,ROLE_USER还是 JSON 数组解析方式不同。规则多了之后建议抽成配置表别硬编码在过滤器里否则改一条规则要重新发网关包。5.2 用 curl 验证整条链路配置完别急着上前端先用命令行把链路走通。三步验证# 第一步用密码模式换 token curl -X POST http://localhost:9000/auth/oauth/token \ -d grant_typepasswordusernameadminpassword123456 \ -d client_idgateway-clientclient_secretsecret-123 # 返回里拿到 access_token # 第二步不带 token 访问应该返回 401 curl -i http://localhost:8080/api/user/info # 第三步带 token 访问应该返回 200 和用户数据 curl -i http://localhost:8080/api/user/info \ -H Authorization: Bearer 上一步拿到的access_token逻辑说明第一步验证认证中心能正常签发第二步验证网关的拦截生效第三步验证网关验签、透传、下游读取整条链路通畅。三步都过说明核心功能没问题。参数说明grant_typepassword对应密码模式client_id和client_secret要和认证中心配置的一致。如果第一步就失败先查认证中心的日志多半是客户端凭证或用户密码不对如果第二步返回 200说明白名单配错了把不该放行的路径放行了。5.3 一个我坚持了很多年的习惯每次改完网关的过滤器或密钥配置我一定先跑一遍上面这三条 curl再打开浏览器测。因为浏览器有缓存、有跨域、有各种预检出问题时你分不清是网关的锅还是浏览器的锅。命令行干净能快速定位问题出在哪一层。从那以后我每次动认证相关配置都强制走一遍换 token → 裸访问 → 带 token 访问这三步省下的排查时间比写这三行命令多得多。这套源码包的价值不在于代码多复杂而在于它把网关认证这条链路里该有的角色、该配的参数、该避的坑都摆出来了。你拿到后先按第 2 章把认证中心跑起来再用第 3 章的过滤器接上网关最后用第 5 章的 curl 验证基本一个下午能跑通。跑通之后再按自己的业务改白名单、加角色规则、决定要不要上 Redis 黑名单。希望帮到你。本文还有配套的精品资源点击获取