从单体应用拆成 SpringCloud 微服务之后,最先崩掉的功能往往不是业务接口,而是登录。Session 存在哪台机器上、用户请求打到别的实例怎么办、网关要不要参与认证、各个服务怎么验证调用者的身份,这些问题不解决,服务拆得越多越乱。这篇文章要讲的,就是一套我踩了不少坑之后沉淀下来的生产级统一认证授权方案:基于 Spring Authorization Server + OAuth2 + JWT,在 SpringCloud 环境下把认证和授权从业务代码里彻底剥出去。
这套方案解决的实际问题很明确:认证统一在授权服务器做,服务之间不再共享 Session,令牌采用 JWT 无状态传输,资源服务只认签名不查会话,权限控制可以下推到接口和角色维度。适合正在从单体向微服务迁移的团队,也适合新项目直接落地,尤其适合那种“登录服务自己写、接口自己校验、到处重复造轮子”的现状需要重构的情况。
1. 项目背景:微服务架构下的认证之痛,到底痛在哪
1.1 单体时代的惯性思维在微服务里为什么会失效
很多团队从单体转微服务,第一个保留的习惯就是 Session。以前 Tomcat 部署单个应用,用户登录后把信息放到 Session 里,请求再来的时候从同一个 Session 里取,一切自然。微服务拆开以后,哪怕只是一个简单的商城系统,拆分出用户服务、订单服务、商品服务,登录请求落在用户服务上,订单服务收到请求却不知道对方是谁。
如果你把 Session 复制到每个服务里,数据同步和一致性就是灾难;如果你用 Spring Session + Redis 集中存储,订单服务和商品服务都能查登录态,看上去解决了问题,但实际上是让所有业务服务都耦合了“用户会话”这个东西。每个服务都要引入 Session 解析逻辑,都要感知 Redis,权限规则散落在各个服务内部,后期根本没法统一管理。
我在某公司重构一个老系统时,就遇到这种场景:两个服务各写了一套登录逻辑,一套用 Session,另一套自己生了个 Token 存在数据库里,前端调用还要根据不同服务切换不同的认证方式。这种状态听起来荒诞,但在真实项目里非常常见。统一认证授权不是锦上添花,是微服务落地绕不过去的基础设施。
1.2 OAuth2 协议为什么是微服务认证的事实标准
OAuth2 解决的问题是“授权”,抽象出来就是:客户端代表用户去访问受保护资源,但客户端不能直接拿着用户名密码到处跑,需要先向授权服务器换取一个令牌,再带着令牌访问资源。在微服务场景下,这个模型天然契合:授权服务器统一负责认证和发令牌,各业务服务变为资源服务器,只负责验证令牌并放行。
OAuth2 的核心不是技术实现,而是一套角色划分和流程约定。搞清楚 Authorization Server、Resource Server、Client、Resource Owner 这四个角色之间的关系,整个认证架构就清晰了。SpringCloud 体系中,认证的工作完全可以收敛到授权服务器,其他服务不再关心“用户怎么登录的”,只关心“令牌是否有效”。这个解耦比用什么技术实现更加关键。
JWT 在这个体系里的角色是令牌的载体。它本身不是协议,而是一种紧凑的、自带签名的令牌格式。资源服务器拿到 JWT,用授权服务器的公钥验签,验签通过就信任里面的用户信息,不需要再发起远程调用检查令牌状态。无状态、去中心化验证,这两个特性正好契合微服务对性能和可用性的要求。
2. 技术选型:为什么是 Spring Authorization Server + OAuth2 + JWT
2.1 从独立认证中心到 Spring Authorization Server 的演进
之前很多团队做统一认证,首选是独立的认证中心,或者自研一套认证服务。自研的路径看起来很“可控”,但真正的复杂度往往超出预期:密码加密策略、令牌生成规则、公钥私钥管理、客户端管理、授权码流程、刷新令牌机制,这些模块看似简单,真正做到生产级,细节量非常大。我在项目里见过不少自研认证服务,令牌就是 UUID,数据库表里存一条记录,也没有过期机制,每次请求都查一次库,性能差不说,安全性也靠不住。
Spring Authorization Server 是官方在 Spring Security 基础上构建的授权服务器实现,它把 OAuth2 流程中那些繁琐的协议细节都标准化了。比如授权码模式的跳转参数校验、Token 端点参数解析、JWT 签名的生成逻辑、客户端信息的存取,都有可扩展的默认实现。你不需要从头写 OAuth2 协议解析,只需要做两件事:配置客户端、定义令牌的生成规则,然后把用户认证的逻辑接入进来。
我自己用下来的感受是,SAS 的上手曲线并不低,但收益很明确。协议层的东西不会再错,你能把精力集中在业务认证方式上,比如接入现有的用户表、对接企业内部的统一身份源、设计多租户场景下的客户端隔离。这些都是自己造轮子时最容易出问题的地方。
2.2 令牌选型:JWT 和 Opaque Token 的权衡
OAuth2 规范的令牌格式并没有限定必须是 JWT,也可以是 Opaque Token(不透明令牌)。这俩的区别值得想清楚。
JWT 是自包含的,用户 ID、角色、过期时间都写在 token 里面,资源服务器本地验签就能确认身份,不需要再回授权服务器查询令牌状态。好处是性能好、服务之间无额外网络依赖;代价是令牌一旦签发,在有效期内很难立刻作废,遇到账号被禁用、密码被修改这种场景,需要配合额外机制才能实现强制下线。
Opaque Token 是一串随机字符串,资源服务器必须通过 introspection 端点到授权服务器换取令牌背后的用户信息。每次请求都多一次远程调用,性能差一些,但令牌可以随时在服务端撤回,细粒度控制更强。
我最终选择 JWT 作为主令牌,原因是微服务场景下请求量集中在服务间调用,每次请求都去授权服务器查一次令牌状态,授权服务器会成为性能瓶颈。JWT 把验证成本降到了本地,服务更像“自治”的个体。至于撤销问题,用短期 Access Token + 刷新令牌组合,再加上必要时的令牌黑名单来兜底,基本可以覆盖大多数生产场景。
2.3 网关在认证链路中的角色划分
网关要不要做认证,这个问题没有标准答案。我的实践是:网关负责“拦截和转发”,核心认证逻辑放在授权服务器,网关做的是识别请求是否携带令牌,以及是否放行到下游服务。
很多人第一反应是让网关去校验 JWT,把用户信息解析出来再传给下游。这种做法的问题在于,如果后续有非 HTTP 协议的调用、有服务间直接调用,网关这层就管不到了。所以更稳妥的搭配是:网关层用 TokenRelay 过滤器把请求里的令牌原样传给下游,资源服务各自配置 JWT 校验。网关只做路径级别的白名单控制,比如登录接口、获取令牌接口不拦,其他统一要求携带令牌。
Spring Cloud Gateway 有现成的 TokenRelayGatewayFilterFactory,加上 Spring Authorization Server 的标准流程,网关基本不需要写太多定制逻辑。核心配置集中在授权服务器和各个资源服务的配置里,路径清晰。
3. 核心架构与生产级配置实现
3.1 整体架构和认证时序
为了方便理解,我把这套方案的组件和流程先梳理一遍。参与的角色有四个:
- 网关:统一入口,负责路由和令牌转发
- 授权服务器:负责登录认证、签发 JWT、刷新令牌,基于 Spring Authorization Server
- 用户服务:承载账号密码校验等认证细节,作为授权服务器的 UserDetailsService 数据源
- 各业务服务:作为 OAuth2 资源服务器,通过 JWK 地址获取公钥,本地校验 JWT
用户访问业务接口的完整流程如下:
- 前端请求网关,访问
/api/user/info,未携带令牌,网关返回 401。 - 前端跳转到登录页,引导用户访问授权服务器的
/oauth2/authorize。 - 用户输入账号密码,授权服务器认证通过后,返回授权码。
- 前端携带授权码向授权服务器的
/oauth2/token端点换取 JWT。 - 后续请求在 Header 中携带
Authorization: Bearer <JWT>,网关透传。 - 业务服务拿到 JWT,用授权服务器的公钥验签,解析用户信息,处理业务。
这套流程的关键在于第 4 步之后,无状态令牌进入系统,后面所有服务都不再依赖 Session。
3.2 授权服务器的搭建步骤
引入 Spring Authorization Server 依赖后,核心配置分为三块:客户端信息、认证方式、令牌配置。
先看依赖引入:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-authorization-server</artifactId> </dependency>这个依赖会带入 Spring Security 的相关组件。建议项目组统一使用同一个 Spring Boot 版本管理依赖版本,避免出现安全组件版本不匹配的问题。
接下来是客户端配置。一个客户端代表一个接入方,比如前端 Web 应用、移动端 App、第三方系统。用代码方式注册客户端比较直观:
@Bean public RegisteredClientRepository registeredClientRepository() { RegisteredClient webClient = RegisteredClient.withId(UUID.randomUUID().toString()) .clientId("web-client") .clientSecret("{noop}web-secret") .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) .redirectUri("http://localhost:8080/login/oauth2/code/gateway") .scope("openid") .scope("profile") .build(); return new InMemoryRegisteredClientRepository(webClient); }注意{noop}前缀表示明文存储密钥,仅适合本地开发。生产环境要使用{bcrypt}加密存储。这个细节很容易被忽略,一旦配置密码加密策略,固定字符串的密钥就必须改成加密后的形式。
客户端配置默认走内存存储,单机够用。如果要部署多实例,就要把 RegisteredClientRepository 替换为基于数据库的实现,否则客户端列表在每台机器上不一致,会出现间歇性认证失败。
令牌配置是重点,直接决定 JWT 的签发与验签方式。创建默认的 JWT 编码器:
@Bean public JwtEncoder jwtEncoder() { JWKSource<SecurityContext> jwkSource = new ImmutableJWKSet<>(jwkSet()); return new NimbusJwtEncoder(jwkSource); } @Bean public JWKSource<SecurityContext> jwkSet() { RSAKey rsaKey = generateRsaKey(); JWKSet jwkSet = new JWKSet(rsaKey); return new ImmutableJWKSet<>(jwkSet); }RSA 密钥对是 JWT 签名的基础。授权服务器持有私钥用于签发,资源服务器只持有公钥用于验签。生产环境中,私钥绝不能写在配置文件里,更不能提交到代码仓库。推荐的做法是把密钥放到专门的密钥管理服务中,或者至少用环境变量注入。
JWK Set 的地址要暴露给资源服务器,Spring Authorization Server 默认提供的/oauth2/jwks端点会自动发布公钥信息。我们只需确保网关或资源服务器能访问到这个地址即可。
3.3 自定义 JWT 声明:把用户信息塞进令牌
默认签发的 JWT 只包含一些标准声明,比如sub、aud、iss、exp。微服务在业务处理中经常需要知道当前用户的 ID、角色、所属租户,这些信息不能每次都调用户服务查询,而是要编码进 JWT 中。
Spring Authorization Server 允许通过 OAuth2TokenCustomizer 定制 JWT 内容:
@Bean public OAuth2TokenCustomizer<JwtEncodingContext> tokenCustomizer() { return context -> { if (context.getTokenType().getValue().equals("access_token")) { Authentication principal = context.getPrincipal(); if (principal instanceof UsernamePasswordAuthenticationToken) { CustomUser user = (CustomUser) principal.getPrincipal(); context.getClaims().claim("userId", user.getId()); context.getClaims().claim("nickname", user.getNickname()); context.getClaims().claim("roles", user.getRoles()); } } }; }这段代码把所有用户关键信息放进了 JWT。需要注意,JWT 是 Base64 编码的,不是加密的,任何拿到令牌的人都可以解码查看内容,所以千万不要把密码、手机号、身份证号之类的敏感字段塞进 JWT。我在一开始设计时犯过这个错误,差点把用户的完整联系方式写进 token,后来被安全评审拦下来了。
复权方面,默认情况下角色信息并不会自动映射到 Spring Security 的authorities中。你在 claims 里放了roles字段,只代表令牌里有这个信息,资源服务器要把它转换成登录态里的权限,必须自定义解析器。
3.4 资源服务器的 JWT 校验配置
业务服务作为资源服务器,配置相对简洁:
spring: security: oauth2: resourceserver: jwt: jwk-set-uri: http://auth-server:9000/oauth2/jwks这个配置让业务服务启动时拉取授权服务器的公钥集合,之后所有 JWT 都在本地验签。注意生产环境不要用域名localhost,要配置为授权服务器在内部网络中的地址,确保网络连通性。
资源服务器的安全过滤链配置:
@Bean public SecurityFilterChain resourceServerSecurityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/public/**").permitAll() .requestMatchers("/admin/**").hasAuthority("ROLE_ADMIN") .anyRequest().authenticated()) .oauth2ResourceServer(oauth2 -> oauth2 .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))); return http.build(); }这里的jwtAuthenticationConverter负责把 JWT 中的角色声明转换为 Spring Security 的权限对象。可以直接利用JwtGrantedAuthoritiesConverter指定 roles claim:
@Bean public Converter<Jwt, AbstractAuthenticationToken> jwtAuthenticationConverter() { JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter(); authoritiesConverter.setAuthoritiesClaimName("roles"); authoritiesConverter.setAuthorityPrefix("ROLE_"); JwtAuthenticationConverter converter = new JwtAuthenticationConverter(); converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter); return converter; }这样配置完成后,业务代码里就可以直接用@PreAuthorize("hasRole('ADMIN')")做接口级权限控制了。
4. 网关集成与服务间调用:把认证能力真正用起来
4.1 Spring Cloud Gateway 的令牌透传
网关在整个认证链路里起着承上启下的作用。用户带着 JWT 访问业务接口,网关要做的不是解析令牌,而是把令牌原封不动地转发给下游服务。
Spring Cloud Gateway 对 OAuth2 提供了很好的支持,只需要在路由配置里启用 TokenRelay:
spring: cloud: gateway: default-filters: - TokenRelay routes: - id: user-service uri: lb://user-service predicates: - Path=/api/user/** - id: order-service uri: lb://order-service predicates: - Path=/api/order/**TokenRelay 过滤器会自动从当前安全上下文中取出 JWT,添加到转发请求的 Authorization Header 中。要注意的是,这个功能依赖网关本身完成了 OAuth2 登录流程,也就是说网关需要配置成 OAuth2 Client。
网关作为 OAuth2 客户端时,配置如下:
spring: security: oauth2: client: registration: gateway: provider: auth-server client-id: web-client client-secret: web-secret authorization-grant-type: authorization_code redirect-uri: "{baseUrl}/login/oauth2/code/gateway" scope: - openid - profile provider: auth-server: issuer-uri: http://auth-server:9000很多团队在第一次配置时容易漏掉网关的 OAuth2 Client 配置,结果发现 TokenRelay 不生效,请求头里根本没有 Authorization。这个依赖关系比较隐蔽,实际调试时可以从网关日志里观察过滤器链路,确认 TokenRelay 是否被正确加载。
4.2 服务间调用如何传递用户身份
HTTP 请求从网关进入服务 A,如果服务 A 需要调用服务 B,这个链路中如何保持用户身份?最简单的做法是把 Header 中的 JWT 透传下去。
可以用 Spring Cloud OpenFeign 的 RequestInterceptor 实现:
@Bean public RequestInterceptor oauth2TokenRelayInterceptor() { return requestTemplate -> { ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attrs != null) { String authorization = attrs.getRequest().getHeader("Authorization"); if (authorization != null) { requestTemplate.header("Authorization", authorization); } } }; }这个方案实现成本最低,所有服务共同信任同一个 JWT 体系,服务 B 拿到令牌后用公钥本地验签,就知道调用者是谁。只要授权服务器不宕机,整条链路的认证逻辑都是自治的。
更进阶的做法是引入内部的 Client Credentials 流程,让服务 A 先获取自己的令牌,再与服务间的用户令牌组合使用。服务 B 既能验证用户身份,又能判断调用方服务是否被授权。不过这个方案复杂度较高,团队对协议不熟的时候不建议一步到位。
4.3 刷新令牌与登录态续期
JWT 的过期时间一般不会设置太长,生产环境常见的是 30 分钟到 2 小时。过期之后用户如果还在操作,就需要刷新令牌来换取新的 Access Token。
OAuth2 的 Refresh Token Grant 流程在 SAS 中已经实现好了,只要客户端配置里授权类型包含REFRESH_TOKEN,令牌端点就支持刷新请求:
curl -X POST http://auth-server:9000/oauth2/token -u web-client:web-secret -d grant_type=refresh_token -d refresh_token=xxxxxx刷新令牌的有效期通常比 Access Token 长得多,比如 7 天甚至 30 天。这里要注意刷新令牌的轮换策略:每次刷新成功后,旧的刷新令牌会被作废,返回一个新的刷新令牌。这是 SAS 的默认行为,目的是降低刷新令牌被盗用的风险。前端拿到新的 Refresh Token 后,必须更新本地存储。
另外要设计好刷新令牌过期后的处理逻辑。如果前端收到invalid_grant错误,说明刷新令牌已失效,应该把用户踢回登录页重新登录。不能让用户卡在一个半死不活的状态里反复点击无响应。
5. 踩坑实录:这些坑我踩过了,你别再踩
5.1 Redis 缓存授权信息时的序列化异常
SAS 默认把授权信息和客户端信息存内存,多实例部署就出问题。一开始我把RegisteredClientRepository和OAuth2AuthorizationService换成 Redis 实现,结果启动没问题,跑授权流程时直接报序列化错误。
原因是 OAuth2Authorization 对象里包含各种复杂类型,默认的 JdkSerializationRedisSerializer 有时候序列化不兼容,于是改成 JSON 序列化,结果 JSON 序列化器对复杂类型也不友好。我最终采用的方式是单独封装一层:在写入 Redis 前,把 OAuth2Authorization 转为自定义的简单 DTO,读取时再还原成标准对象。这个做法虽然多一点代码,但稳定性和可调试性都更好。
如果你用的是 Spring Data Redis 的默认GenericJackson2JsonRedisSerializer,务必对授权对象做类型信息的兼容处理,否则反序列化时会出现各种隐晦的异常。
5.2 资源服务器验签失败:JWK 地址和时钟问题
资源服务器的常见报错是JwtValidationException或签名不匹配。排查方向有两个。
第一个方向是公钥对不上。授权服务器每次重启,如果 RSA 密钥是随机生成且没有持久化,那么重启后公钥就变了,资源服务器如果缓存了旧公钥,验签就会失败。这就是为什么生产环境必须持久化密钥,不能每次生成新的。
第二个方向是系统时钟偏差。JWT 有nbf(not before)和exp(expiration)两个时间约束,如果资源服务器和授权服务器所在机器的时钟差了几分钟,可能出现令牌“还没生效”的情况。容器化部署时这个问题尤其明显,需要确保所有节点使用同一时间源,推荐在基础镜像里配置 NTP 同步。
5.3 跨域导致前端拿不到令牌
前端和后端分离部署时,CORS 问题几乎一定会遇到。授权服务器的 Token 端点和登出端点都需要允许浏览器的跨域请求。
SAS 是构建在 Spring Security 之上的,CORS 配置要往 SecurityFilterChain 里加:
http.cors(cors -> cors.configurationSource(corsConfigurationSource()));注意不要在网关层乱配 CORS,很多场景下网关和前端同域或者通过代理转发,配置不当反而会导致预检请求直接失败。最好明确一条链路负责 CORS,其他环节不要重复处理。
5.4 角色权限解析为空
JWT 里明明有 roles 字段,资源服务器接口却一直返回 403,这是最常见的权限问题。
问题通常出在JwtGrantedAuthoritiesConverter的角色前缀和授权表达式不匹配。JWT claims 里存的是"roles": ["admin", "user"],如果不设置setAuthorityPrefix("ROLE_"),得到的权限名是admin,而hasRole("ADMIN")实际查找的是ROLE_ADMIN,自然就匹配不上。此外,claims 里的值是大写还是小写也要统一,建议在授权服务器生成 roles 时直接统一为标准格式,省得资源服务器再去转换生事。
5.5 退登和令牌撤销
JWT 无状态是双刃剑。要强制某个用户退出,比如账号被封禁,JWT 本质上没法立刻失效。生产环境常见的兜底方案是维护一个黑名单。
做法是给 JWT 加一个唯一的jti声明,强制退登时把jti写入 Redis,设置过期时间为令牌剩余有效时间。资源服务器验签通过后,先查一下黑名单,如果存在就拒绝请求。这个逻辑可以封装成自定义的JwtAuthenticationConverter的一部分,或者写一个过滤器统一处理。
这套逻辑的代价是多一次 Redis 查询,但只在退登场景才有额外开销,正常情况下不做黑名单校验,性能影响可控。如果要求每次请求都能即时检测封禁状态,那就要牺牲 JWT 的无状态特性,改用 Opaque Token 方案——这两个取舍没法既要又要,必须在设计阶段想清楚。
6. 生产环境安全加固与性能优化建议
6.1 签名算法与密钥管理规范
JWT 签名算法建议使用 RS256、RS512 这类非对称算法,私钥只存在授权服务器,资源服务器只用公钥验证。对称算法 HS256 虽然实现简单,但所有资源服务器都要持有同一个密钥,密钥一旦泄露就能伪造任意用户的令牌,扩散性太强。
RSA 密钥的位数,推荐 2048 位起步。密钥轮转方面,可以定期更换 JWK 集合中的签名密钥,JWK 格式本身支持kid标识,资源服务器可以通过kid选择对应公钥验证。实现轮转逻辑时,要保证新旧密钥并行一段时间,等所有资源服务都拿到新公钥后再下线旧密钥。
6.2 令牌有效期和刷新策略的最佳实践
Access Token 的有效期绝对不是越长越好。时间越长,被偷走后的风险窗口越大。我一般建议:
- Access Token:30 分钟到 2 小时,取决于业务操作频率
- Refresh Token:7 天到 30 天,也取决于产品对登录态持久度的要求
- 开启 Refresh Token 轮换,每次刷新后旧刷新令牌立即失效
- 对刷新令牌做设备维度或 IP 维度的绑定,异常见则拒绝刷新
如果业务上有“记住我”这种需求,可以延长 Refresh Token 的有效期,但必须配合更严格的安全检测,比如异常登录提醒、新设备验证。
6.3 接口级权限控制拆分
统一认证只是第一步,授权还需要落到接口维度。资源服务器里可以通过方法级别安全注解实现:
@PreAuthorize("hasRole('ADMIN') or hasRole('OPERATOR')") @GetMapping("/orders/{id}") public OrderVO getOrder(@PathVariable Long id) { ... }而数据级别的权限,比如“只能查看自己的订单”,靠 JWT 里的 userId 配合业务查询条件完成。角色权限的映射关系建议集中配置,避免把权限规则散落在各个服务里,否则后期改一次权限就要改多个服务,运维起来很痛苦。
角色和权限的动态管理可以引入权限中心服务,业务服务只校验权限字符串,是否拥有该权限由权限中心统一下发。这个方案做起来重,但对权限模型复杂的公司特别有用。
6.4 性能瓶颈:哪些地方可能出现高延迟
JWT 方案在性能上最大的优势就是验签本地化,通常在毫秒级。但有几个地方会成为瓶颈。
授权服务器承担所有登录和令牌签发请求,高并发场景下要注意 Tomcat 线程池和数据库连接。/oauth2/token端点的压力可以通过缓存减轻,比如客户端认证信息的查询加缓存,用户认证信息的查询也可以做二级缓存。
资源服务器首次启动拉取 JWK Set 会有一个短暂的初始化延迟,之后会缓存公钥集合。如果授权服务器地址发生变化,资源服务器需要重新拉取,这时候可以配置定时刷新点,避免公钥长期不更新。
如果用户量非常大,又要求每个请求都能即时生效令牌撤销,最有效的方案是在网关做黑名单缓存检查,把 Redis 查询集中到网关一层,业务服务完全无状态。
7. 最后的实操心得
这套 Spring Authorization Server + OAuth2 + JWT 方案,整体落地下来最深的体会是:技术本身不是最难的,最难的是把协议和流程理顺。OAuth2 的授权码模式、JWT 的声明设计、密钥的生命周期管理,这些概念如果不彻底理解,配置代码能跑通,但出了故障一脸懵。
第一次做的时候,建议先不要急着写代码,花一天时间把角色关系图画一遍:谁拥有什么资源、谁来签发令牌、谁来验证令牌、前端在什么场景走什么流程。画清楚了再动手,后面基本是一路顺风。
有条件的话,把授权服务器单独做一个模块,不要和业务服务混在一个代码库。密钥管理、客户端管理、令牌策略这些变更,未来频率会比业务需求高得多,独立模块能让你更安全地做变更。
最后再分享一个实用的技巧:生产环境给资源服务器加一个统一的审计日志,把每次请求的 userId、访问路径、JWT 的 jti 记录到日志中心。出了安全事件,如果没有审计日志,排查范围会大到你怀疑人生。这个习惯我从第一次上线微服务认证体系时就开始坚持了。