资讯动态

微服务安全实战:OAuth2与JWT在Spring Cloud中的统一认证方案

发布时间:2026/10/18 5:57:55 来源:尧图企业网站定制
1. 从单体到微服务安全这块为什么这么头疼做微服务的第一天你多半会关心服务注册、配置中心、网关这些基础组件。等这些跑通之后真正让人睡不着觉的反而是安全认证。单体应用时代用户的登录状态丢在 Session 里顶多多配几个 Tomcat 的 Session 同步策略或者干脆用 Redis 做 Session 共享问题不大。但拆成微服务之后情况立刻变得不一样了。用户的一个请求要经过网关再转发到订单服务、库存服务、用户服务甚至底层还要调用户中心。每个服务都去处理“这个请求是谁发来的、他有没有权限调我”整个架构就会乱套。这时候你需要一套统一的、标准的、可扩展的认证授权机制。我这两年做过的几个分布式项目里OAuth2 几乎是唯一稳妥的答案配合 JWT 做无状态 Token既能让各业务服务保持轻量又能实现认证逻辑的统一收敛。如果你正在从单体向 Spring Cloud 微服务过渡或者已经拆了服务但安全模块还停留在“每个服务自己查 Session”的状态这篇文章就是我在这类项目上反复踩坑之后的实战总结。我会把 OAuth2 的关键概念、授权服务器与资源服务器的搭建流程、Token 签名与校验、网关集成方案以及我实际排查过的高频问题一次性讲透。内容偏实操代码片段都是可以直接落地改一改就用的适合已经跑通 Spring Cloud 基础组件的团队参考。1.1 会话共享与分布式 Session 的老问题先说说我最早接手的一个项目。那是一个电商类系统服务拆了七八个安全方案还是老一套用户登录之后把信息写进 Redis每个服务自己拿 Cookie 里的 SessionId 去 Redis 查用户。听起来没毛病但实际跑起来全是坑。最大的问题是 Cookie 的作用域。前端页面在 A 服务下接口在 B 服务下浏览器不会自动把 A 域下的 Cookie 传给 B 域。你要么做跨域转发要么在前端手动拼 SessionId 到请求头怎么看都别扭。第二个问题是服务端没有状态边界任何一个服务都可能被负载均衡打到任意一台机器上SessionId 查到查不到取决于 Redis 是否挂了、网络是否抖动。第三个问题是跨调用传递。服务 A 调用服务 B 时B 想拿当前用户身份还得把 SessionId 塞进内部接口参数里调用链一长整个代码里到处是 userId、userName 这种隐式参数维护起来非常痛苦。我当时就被这种方案折磨得不轻。后来换成 Token 方案身份信息不再依赖服务端存储请求方直接带一个经过签名的 Token 过来服务端只需要验签、解出身份和权限即可。服务之间调用时Token 可以通过请求头透传底层服务也能轻松知道调用者的身份不再需要把用户信息拆成一堆参数传来传去。1.2 Token 化改造为什么微服务普遍选择 OAuth2Token 化听起来简单自己定义一个 Base64 串、加个签名似乎也能用。但真正落到工程上你会发现需要解决的问题远不止“能否验签”多端授权、刷新续期、第三方接入、客户端区分、权限范围控制这些需求如果全部自己造轮子工作量会非常恐怖。OAuth2 的价值就在于它把这些场景做了标准化。它定义了客户端、授权服务器、资源服务器三个核心角色把“谁在用”“用什么方式授权”“能访问哪些资源”这几个问题清晰地拆开。Spring Security 从早期版本开始就内置了对 OAuth2 的支持生态非常成熟配置项和过滤器链路都现成你不需要从零实现签名、校验、密钥轮换这些底层逻辑只需要做业务层面的适配。可以这么理解单体时代的登录相当于你去公司门口保安室登记一次之后整个园区都认你这张脸微服务时代的 OAuth2 更像你去健身房前台给你发一张门禁卡每个房间门口的闸机都认识这张卡不需要每个房间再备一份你的纸质档案。后续不管新增多少个服务只要它们都认同一套验证规则安全边界就能统一管理。2. OAuth2 协议拆解四个角色、四种模式、一个核心概念OAuth2 本身不是一个很复杂的协议但它名词多第一次接触容易晕。我先帮你把核心概念捋一遍。参与方一共四个资源所有者一般就是用户、客户端如一个前端网页、一个小程序、一个第三方 APP、授权服务器负责登录和发 Token、资源服务器持有用户数据比如订单服务、用户服务。注意客户端不一定有界面也可能是后端服务自己扮演客户端。端点则有四个最重要的授权端点authorize、令牌端点token、JWKS 端点用来暴露公钥、用户信息端点userinfo。整个流程基本就是围绕这些端点展开的。授权码模式是 OAuth2 中最严谨、也是应用最广泛的模式流程大致是客户端把用户引导到授权服务器的登录页用户登录并同意授权授权服务器回调客户端并附带一个一次性授权码客户端用这个授权码去令牌端点换取真正的 Access Token 和 Refresh Token。2.1 授权码模式是默认选项但它不是唯一选项很多人误以为 OAuth2 就是“第三方登录”其实第三方登录只是客户端类型的一种。在微服务内部常见的授权模式有三种。授权码模式适用于有前端页面的 Web 应用、移动端应用。用户会经历一次完整的登录动作授权码只在浏览器和服务端之间流转安全性最高Refresh Token 也会正常签发适合前后端分离项目的用户认证场景。客户端模式适用于没有用户参与的服务间调用。比如定时任务调度服务要调订单服务的统计接口或者网关内部要去用户服务拉取某个用户的手机号。客户端配置好 client-id 和 client-secret直接向令牌端点申请一个属于自己身份的 Token不需要用户登录。密码模式在 Spring Authorization Server 新版本里已经不支持了但老项目里还大量存在尤其是纯后端接口类的单体系统。我经手过一个老项目改造成微服务时还是靠自定义实现了一个密码模式端点因为前端和后端是强耦合的内部系统用户量也不大直接拿用户名密码换 Token 最省事。如果你在维护这类系统建议尽早把它往授权码模式或客户端模式上迁移密码模式把口令直接暴露给客户端风险上限太高。这三种模式的选择原则其实很简单有没有真实用户参与决定你要用授权码还是客户端模式前端形态决定你能不能安全保管 client-secret。如果前端是纯浏览器 SPAclient-secret 是不应该出现的你要么用授权码加 PKCE要么把换取 Token 的步骤放到你自己的后端网关去做。2.2 JWT 与不透明 Token 的取舍以及 Token 的存储问题OAuth2 协议并没有规定 Access Token 的格式你可以返回一串 UUID也可以返回一个 JWT。这个选择直接影响整个微服务链路的设计。不透明 Token 就是一顿乱码资源服务器拿到之后没法自己解析必须回调授权服务器去验证。每来一个请求服务就得发起一次网络调用延迟高、吞吐量低但它有一个优点授权服务器能随时让 Token 失效。吊销、踢人、权限变更修改授权服务器里对应的记录即可立刻生效。JWT 则把用户身份、有效期、权限范围全部编码进 Token 本身资源服务器只要验签通过就能直接取出发行者、用户ID、角色权限无需再回调授权服务器天然适合高并发的微服务场景。代价是 Token 一旦发出在过期前很难主动吊销——不过这个痛点可以通过缩短 Access Token 有效期、搭配 Refresh Token 轮换机制来缓解。我的建议是对外部第三方开放 API 时用不透明 Token 更利于统一控制面向自身微服务集群内部用 JWT 更舒服。Spring Security 对两种方式都有封装你在资源服务器配置里改了校验器就行业务代码几乎不用变。JWT 的存储是另一个容易被忽视的点。很多团队习惯把 JWT 全量存在 Redis 里这种做法不是不行但会削弱 JWT 无状态的优势而且每次请求都要查 Redis性能和可用性都会打折扣。我常用的策略是Access Token 不落库完全靠签名和有效期校验Refresh Token 必须落库或落 Redis因为刷新操作是低频的而且需要支持轮换和吊销。这样既保住性能又解决了“用户改密码后旧 Token 还能用”的安全缺口。3. 授权服务器搭建从依赖到签发 Token 的完整实操我在早期项目里用的是旧的 Spring Security OAuth2 官方组件配置方式是基于注解的 EnableAuthorizationServer当时感觉还挺顺手。但那个组件早已停止维护新项目再用它纯粹是给自己埋雷。现在官方推荐的是 Spring Authorization ServerSpring Boot 3 时代的标准方案。如果你的项目还在用 Spring Boot 2.x那你可能需要谨慎处理版本兼容我后来经手的某个项目就是 Boot 2.7 搭配 Spring Authorization Server 的某个中间版本跑通的锁定版本很重要。3.1 依赖选型与版本匹配依赖没有想象中那么多核心就两个Spring Web 相关的基础依赖以及 spring-boot-starter-oauth2-authorization-server。需要注意Spring Authorization Server 不是 Spring Security 的一部分二者是独立发布的版本必须和 Spring Boot 对齐。举个例子我的某个模拟项目 X 用的组合是 Spring Boot 3.1.x Spring Security 6.1.x Spring Authorization Server 1.1.x这套组合官方适配得比较好踩坑少。如果你的资源服务器是独立工程还需要引入 spring-boot-starter-oauth2-resource-server。这个依赖会让 Spring Security 自动找 JwtDecoder 的实现配置好 issuer-uri 或公钥之后过滤链就会自动对所有受保护路径做 Token 校验。3.2 授权服务器核心配置在 Spring Authorization Server 里最重要的一步是把客户端信息声明出来。客户端可以存在内存里也可以存到数据库。我建议从内存配置起步先把流程跑通再慢慢迁移到数据库表结构毕竟客户端管理一般不会频繁变更多少条记录。spring: security: oauth2: authorization-server: client: registered: demo-client: client-id: demo-client client-secret: {noop}secret client-authentication-methods: client_secret_basic authorization-grant-types: authorization_code,refresh_token,client_credentials redirect-uris: http://127.0.0.1:8080/login/oauth2/code/demo scopes: openid,message.read,message.write token-setting: access-token-timeout: 30m refresh-token-timeout: 12h这里有几个容易踩雷的细节。client-secret 如果是明文必须用 {noop} 前缀否则 Spring Security 会把它当 BCrypt 密文去匹配。实际生产环境肯定不能用明文我一般会把 secret 用 BCrypt 加密后写在配置中心里初始化客户端时也统一走加密后的值。scope 不要随便乱写它代表客户端能被授予的权限边界宁可少给、按需给。光有配置还不够你需要显式声明授权服务器的 SecurityFilterChain它跟普通资源服务器的过滤链是两条独立的链路。Bean Order(1) public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); return http.build(); }这个过滤链是授权服务器自己的入口负责处理 /oauth2/authorize、/oauth2/token 这些端点。Order(1) 很关键保证它优先于普通认证链路执行否则授权服务器端点会被默认的登录流程拦截。我在第一次搭建时忘了声明这个 Bean结果访问授权页面直接跳到了默认登录页半天没反应过来。3.3 Token 的安全细节过期时间、密钥、刷新逻辑Access Token 过期时间我一般不建议超过 30 分钟我见过许多团队为了“让用户少登录几次”直接把 Access Token 设成 24 小时这等同于把长期通行证发给所有人一旦 Token 泄露攻击者的操作窗口大得吓人。Refresh Token 则可以给到 12 小时甚至 7 天配合刷新策略用户体验和安全性都能兼顾。密钥管理是整个 OAuth2 方案里最核心的安全性话题。开发环境里用对称密钥 HS256 图方便授权服务器和资源服务器共用一个 secret生产环境我强烈建议换成非对称密钥 RS256。授权服务器用私钥签名资源服务器只需要保存公钥即使某个业务服务的公钥泄露攻击者也无法伪造合法 Token。私钥则留在授权服务器内部配合密钥轮换机制定期替换。Spring Authorization Server 默认会自动生成一个 RSA 密钥并暴露到 /oauth2/jwks 端点开发环境可以直接用这份默认密钥但每次重启都会变化。生产环境我会配置一个固定的密钥对或者把 JWK Set 放到配置中心管理避免重启后所有 Token 失效。JWT 的过期时间还可以细化。比如 Access Token 设 30 分钟Refresh Token 设 12 小时。刷新时我倾向于轮换模式——每次刷新都发一个新的 Refresh Token并把旧的作废这样即使用户的 Refresh Token 被窃取攻击者也只能用一次。Spring Authorization Server 默认就支持轮换你只需要保证刷新接口没有关闭即可。4. 资源服务器与网关集成让所有服务都认得这张 Token授权服务器搞定之后真正要铺开的是各个业务服务。每个业务服务本质上都是一个资源服务器它们的任务很简单校验 Token、取出用户信息、判断权限。听起来简单但配置不对光“验签”就能给你折腾出无数个 401。4.1 资源服务器的核心配置资源服务器配置最关键的是告诉 Spring Security 去哪里拿公钥。最简单的方案是配置 issuer-uri服务启动时自动去授权服务器拉取公钥。spring: security: oauth2: resourceserver: jwt: issuer-uri: http://auth-server:9000这种配置在授权服务器和资源服务器都部署在同一内网时非常方便你只需要保证授权服务器地址可达。另一种方式是直接把公钥文件放到本地适合授权服务器公网地址不稳定或跨云的场景。spring: security: oauth2: resourceserver: jwt: public-key-location: classpath:keys/rsa-public.pem如果你用的是 RSA公钥和私钥是一对PEM 文件里的内容不能随便拼接。我踩过一次很蠢的坑把私钥文件内容粘贴到了公钥配置里结果所有 Token 校验全部失败。还有一次是公钥少复制了一行Base64 解码直接报错。规范做法是部署时生成一次密钥对授权服务器配私钥每个资源服务器单独下发公钥文件并定期轮换。资源服务器还需要一个 JwtDecoder 实例。Spring Boot 的自动配置在检测到 resourceserver.jwt 配置后会自动创建但如果你想在 JWT 里自定义字段就需要手动声明解码器。Bean public JwtDecoder jwtDecoder() { NimbusJwtDecoder decoder NimbusJwtDecoder.withPublicKey(publicKey).build(); decoder.setClaimSetConverter(new MappingClaimSetConverter()); return decoder; }这里我绕远一点说如果你的授权服务器不是自己搭的而是第三方云服务你还需要配置 Jwt 时间校验的宽松度因为不同机器的时钟可能存在几秒偏差默认的严格校验会把提前几秒使用的 Token 直接拒掉。4.2 网关统一鉴权 vs 服务自行校验到了微服务架构里一个老问题会摆到你面前Token 校验到底放在网关做还是放在每个业务服务做我的结论是两层都做但职责不同。网关负责流量的入口校验检查 Token 是否存在、签名是否可信、客户端是否拉黑校验通过后把用户信息放入请求头转发给下游。业务服务负责细粒度的权限控制比如某个接口只允许管理员调用、某个订单只允许本人查看这属于业务层的判断网关不可能替服务做完。网关的过滤器实现我在模拟项目 X 里用过两种方式。一种是用 Spring Cloud Gateway 自带的路由过滤器做全局校验简单粗暴适合校验逻辑不复杂的场景。另一种是自定义 GlobalFilter把自定义校验逻辑和后端服务需要透传的信息都封装在内。后者的代码大致是Component public class TokenRelayGlobalFilter implements GlobalFilter, Ordered { Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request exchange.getRequest(); ListString authHeaders request.getHeaders() .get(HttpHeaders.AUTHORIZATION); // 白名单路径直接放行 if (isWhitelisted(request.getURI().getPath())) { return chain.filter(exchange); } // 校验 Token解析用户标识 Claims claims parseToken(authHeaders.get(0)); ServerHttpRequest mutatedRequest request.mutate() .header(X-User-Id, claims.get(userId, String.class)) .build(); return chain.filter(exchange.mutate().request(mutatedRequest).build()); } Override public int getOrder() { return -100; } }网关校验通过后下游服务可以直接信任 X-User-Id 这个请求头。但这里有一个安全前提业务服务的入口必须隔离在外网之外不能越过网关直接暴露到公网否则任何人都能伪造 X-User-Id 头。我在生产环境会额外加一道检查业务服务只监听内网端口仅网关能访问。4.3 用户信息传递的几种姿势Token 校验通过身份信息拿到了怎么在各服务之间传递也有讲究。如果所有业务服务都做 JWT 验签每个服务都能自己从 Token 里解析 userId、userName 和角色权限下游不需要上游额外传任何参数这种模式最自然。缺点是要保证所有服务都配置了同一个 JWT 公钥源。网关统一解析用户信息转成请求头传给下游这种模式适合你不想在每个服务里重复做 Token 解析的场景。但请求头被篡改的防护要靠网络隔离来完成对部署要求更高。内部服务调用用户中心业务服务不持有用户完整信息需要根据 userId 实时拉取这种模式适合用户信息结构复杂、Token 里塞不下的场景。比如一个用户有几十个角色、几百个岗位全塞进 JWT 会让 Token 体积膨胀得很夸张通常的做法是在 JWT 里只放 userId 和权限版本号业务服务按需查用户中心。我在做权限设计时会把角色权限做两层JWT 只声明 userId 和基本角色真正的菜单权限、按钮权限从权限服务实时查询。这样 Token 体积小权限变更也能实时生效而不是等到 Token 过期才刷新。唯一要留意的是实时查询的缓存策略我用的是本地 Caffeine 加 Redis 两级缓存权限变化时手动清缓存或依靠版本号失效实测下来稳定性很好。5. 实战中踩过的坑问题排查与经典故障复盘说实话理论再完整也不如实际故障一次。下面几个问题都是我在真实环境里排查过的每个都让团队花了不少时间。我把它们整理成速查表后面再逐个展开。| 故障现象 | 根因 | 排查路径 | | --- | --- | --- | | 所有资源服务器接口返回 401 | 授权服务器和资源服务器的密钥不一致 | 对比 jwks 是否更新检查公钥配置 | | 只有部分服务报 401另一部分正常 | 资源服务器本地公钥文件过期或配置错误 | 对比各服务公钥内容检查启动日志 | | 登录后很快失效需要反复登录 | Access Token 过期时间设置过短 | 调长 Access Token 有效期检查 Refresh Token 逻辑 | | 网关转发后业务服务拿不到用户信息 | 过滤器没把自定义 header 加入转发白名单 | 检查路由配置和过滤器代码 | | 白名单路径仍被拦截 | 路径规则写错或过滤链顺序不对 | 用 AntPathMatcher 匹配规则测试调大过滤器优先级 | | 上线后 Token 全部 401 | 授权服务器密钥轮换资源服务器未同步 | 重启资源服务器重新拉取公钥或配置 jwk-set-uri 自动刷新 |5.1 Token 签名不一致导致排查半小时的 401有个项目上线第二天订单服务所有接口开始 401其它服务正常。服务日志里只显示“Invalid signature”没有更多线索。我先看授权服务器日志确认 Token 是正常签发的再看资源服务器配置公钥文件路径也没错。最后把授权服务器 /oauth2/jwks 返回的公钥和资源服务器本地的公钥做了一次逐字节对比才发现运维在发布时把旧环境的公钥文件带到了新环境刚好那一次授权服务器完成了密钥轮换两边就错位了。遇到这类问题先把环境变量、配置文件、密钥文件的变更记录翻一遍远比盯着日志猜有效。生产环境如果用了密钥轮换资源服务器最好配置 jwk-set-uri 自动拉取别用固定公钥文件的思维去应对动态密钥。5.2 白名单路径失效路径匹配规则和过滤器顺序的坑白名单路径失效是我见过频率最高的问题特别是在网关层。有些团队把白名单写进 yaml用通配符配置路径但 Spring Cloud Gateway 的路径匹配规则和你在旧 Spring MVC 里用的 AntPathMatcher 不完全一致导致 /auth/login 明明在列表里还是被拦截。我在某个项目里排查了半天结果发现是 yaml 里的路径末尾多了个斜杠。还有一个几乎必然踩坑的点是过滤器顺序。如果全局认证过滤器的 order 设置得比路由过滤器大它可能不会按你期望的顺序执行。我的习惯是全局过滤器用负数优先级统一设为 -100 或更小的值确保它在其它过滤器之前执行白名单判断放在过滤器最前面一旦命中就不再进入校验逻辑。5.3 网关转发的隐性丢失请求头没传下去Token 校验通过后网关会往请求头里注入 X-User-Id、X-Client-Id 这些信息但业务服务经常收不到。起初我以为是代码逻辑没生效后来发现是 Spring Cloud Gateway 路由配置里没有显式转发对应头。spring: cloud: gateway: routes: - id: order-service uri: lb://order-service predicates: - Path/api/order/** filters: - DedupeResponseHeaderAccess-Control-Allow-Origin - RemoveRequestHeaderCookie需要检查的地方有两个。一是自定义过滤器是否在 mutate 之后真正把 header 加进了 exchange二是路由中是否有 RemoveRequestHeader 过滤器把请求头删掉了。比如有人习惯在网关处把 Cookie 头去掉但如果你依赖 Cookie 做会话逻辑这里就会闯大祸。我在实际项目中遇到的是另一种情况下游服务同时接入了网关直连和内部调用两个入口内部调用没有经过网关自然拿不到 X-User-Id导致服务里代码不一致。5.4 微服务间的内部调用鉴权该怎么做微服务之间经常相互调用比如订单服务调用用户服务获取用户地址这种调用如果直接透传用户的 Access Token一方面可能超过 Token 的 scope 范围另一方面也容易误判用户身份。我的建议分两种场景处理。内部系统之间有业务上下文的调用用 Feign 拦截器透传当前用户的 Token这样底层服务能感知到调用者身份适合用户主动操作触发的链路。定时任务、系统间同步这类没有用户参与的调用用 OAuth2 客户端模式申请一个服务级别的 Token。为每个服务配置一个独立的 client-idscope 收紧到最小范围。我在实际项目里给用户同步服务单独设置了 client_credentials 的授权类型只授予了 user.read 这个 scope即使 Token 泄露也只能读用户信息不能做任何写操作。还有一个细节Feign 的拦截器在处理请求头时要注意 ThreadLocal 的传递。因为 Hystrix 或某些异步线程池会切换线程直接把请求头丢在 ThreadLocal 里很容易取不到值。我用过一个叫 RequestContextHolder 的工具类解决这个问题但更稳妥的方案是把当前用户信息塞进自定义的上下文对象里并在发起内部调用前显式传递。6. 写在最后权限不是把所有接口锁死就算完整个 OAuth2 体系跑通之后你会发现在 Spring Cloud 里做安全认证没有想象的那么复杂关键是把角色拆清楚授权服务器只管登录和发牌资源服务器只认牌不认人网关负责入口秩序业务服务管细粒度权限。各司其职链路就会清晰很多。但我最后想多说一句安全设计最怕一刀切。我见过太多团队把所有接口一股脑全放网关后面结果每个页面的加载都要经过五六次 Token 解析性能下降明显也见过权限模型过度设计把几十个角色塞进 JWT导致 Token 体积膨胀到几 KB。我个人的实践习惯是三层控制第一层网关校验 Token 合法性第二层业务服务用 PreAuthorize 判断角色权限第三层在关键数据操作上用代码做归属校验比如订单必须属于当前用户。三层配合既不牺牲性能也把误操作和越权访问挡在了外面。如果你是从零搭建这套体系我建议先走通授权码模式加 JWT 的无状态链路再考虑密钥轮换、刷新轮换、网关透传等进阶内容。安全这件事基础链路稳了后面才好加料。我踩过的坑基本都写在上面的排查表里了你大概率会比我顺利。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑