
做后端开发这几年Spring Security 和 OAuth2 这两个词几乎绕不开。尤其是微服务架构铺开之后“登录认证”早就不是简单加个 Filter 链、存个 Session 就能糊弄过去的事情。这次的项目就是在已有 Spring Security 基础登录之上扩展出一套完整的 OAuth2 授权流程走的是当前主流的技术组合Spring Authorization Server 充当授权服务器业务服务作为资源服务器再加一个前后端分离的客户端应用。整个过程跑下来从授权码到令牌、从 JWT 签名到方法级权限控制每个环节都能落地验证。这篇实战总结会把完整路径拆开为什么选这套方案、授权服务器和资源服务器各自怎么配置、令牌在联调中怎么验证以及我踩过的几个典型坑。适合已经用过 Spring Security 做基础登录、想进一步理解 OAuth2 授权机制或者正准备在公司项目里接统一认证平台的开发同学。1. 这个“扩展”到底扩了什么OAuth2 在 Spring Security 里的位置1.1 从 Session 到 Token为什么要走 OAuth2Spring Security 传统的认证方式核心是 Session。用户在登录页提交用户名密码服务端校验通过后把用户身份存进 HttpSession再通过 Cookie 把 Session ID 塞回浏览器。这种方式在单体应用里非常稳但一旦拆成微服务问题就来了服务 A 登录了服务 B、服务 C 怎么知道这个用户是谁总不能每个服务都做一次登录校验更不能要求每个服务都去共享一份 Session 数据。Token 方案解决了这个痛点。用户只需要和认证服务打交道拿到一个 Token之后所有业务服务都通过校验这个 Token 来确认身份服务本身不用保存任何会话状态。OAuth2 之所以比“自己发明一个 Token 系统”靠谱是因为它把“认证”和“授权”这两个动作规范化了。你可以把它类比成酒店房卡前台核实身份后发卡餐厅、健身房、电梯都只认卡不认人而这个卡能刷开哪些门是由前台的授权策略决定的。OAuth2 在 Spring Security 生态里的定位就是这一层的“发卡机”和“读卡器”。Spring Authorization Server 负责发卡也就是颁发令牌各个业务系统通过配置资源服务器逻辑来读卡校验令牌是否有效、包含哪些权限。Spring Security 本身擅长的是“认证上下文”的管理OAuth2 则把认证的边界扩展到多个服务之间。1.2 一个真实的业务场景还原这次实战的背景是一个中小型电商后台已经用 Spring Security 做了运营人员的管理端登录登录后存 Session权限用角色控制。后来业务方提了一个需求供应商要能通过合作方平台自助查询订单数据他们不希望给供应商单独开一套账号体系而是希望让合作方平台自己维护账号供应商在合作方平台登录后授权我们这个系统读取他名下的订单。这个需求如果继续用 Session 方案等于要把合作方平台的用户信息同步到我们系统既麻烦又不安全。用 OAuth2 授权码模式就顺理成章合作方平台作为客户端跳转到我们的授权服务器供应商输入合作方平台的账号密码完成授权授权服务器把授权码返回给合作方平台合作方平台再拿授权码换令牌之后带着令牌来查订单。这就是这次项目的基本雏形。实际的开发过程中我还把运营人员原本的登录也逐步迁移到了 OAuth2 体系里让管理端和服务端、供应商接入走同一套令牌机制只是 scope 和权限不同。这样做的收益很明显统一认证入口、统一令牌校验逻辑、日志审计也集中到一处。2. 动手前的关键抉择技术选型与核心概念2.1 版本大坑旧 OAuth2 栈已废弃新方案这么选Spring Security OAuth2 这块的版本历史是很多新手绕不过去的坎。如果你在网上搜索资料很容易搜到一堆基于EnableAuthorizationServer、EnableResourceServer的代码示例那些是 Spring Security OAuth 项目里的旧写法。这个老项目目前已经进入维护冻结期Spring 官方已经明确不再建议新项目使用对应的自动配置模块spring-security-oauth2-autoconfigure在 Spring Boot 2.x 后期版本里也标记为废弃。现在的主流方案是 Spring Authorization Server这是一个独立于 Spring Security 的项目专门负责授权服务器能力从 Spring Security 5.x 后期开始配合使用。资源服务器能力则直接内置于 Spring Security 框架本身通过oauth2ResourceServer()配置开启。注意Spring Authorization Server 不是一个自动配置库它需要手动装配很多 Bean这既是它的门槛也是它的灵活之处。我在实践中建议的版本组合是这样的Spring Boot 3.x Spring Security 6.x Spring Authorization Server 1.x。这组搭配在 2024 年以后非常稳定文档也齐全。如果团队还在用 Spring Boot 2.7也可以选 Spring Security 5.8 Spring Authorization Server 0.4.x但那个版本段的资料偏老建议能升级尽量升级。2.2 授权模式选型为什么是授权码模式OAuth2 定义了四种授权模式授权码模式、简化模式、客户端凭据模式、密码模式。看过不少项目的设计大多数直接选密码模式把用户名密码直接 POST 给令牌端点换 Token开发确实省事但仅仅适用于第一方应用——也就是我们完全信任的自家前端。对于前面提到的供应商通过合作方平台接入的场景合作方平台不是我们能管控的代码用户名密码绝不能交给他们授权码模式才是唯一合理的选择。授权码模式的核心逻辑是“授权码中转”。第三方客户端先把用户引导到授权服务器用户完成身份确认并同意授权后授权服务器通过浏览器重定向给客户端一个一次性授权码客户端再拿着授权码通过后端渠道换取 Token。这个过程中用户名密码只流经授权服务器第三方客户端全程接触不到安全性有保障。图中是授权码模式的完整时序用户访问客户端客户端把用户重定向到授权服务器授权服务器要求用户登录并确认授权确认后重定向回客户端并附上授权码客户端后端用授权码换令牌之后所有 API 请求都携带令牌。注意授权码模式里最关键的一个配置项是redirect_uri它必须和客户端注册时完全一致否则授权服务器会直接拒绝。这个参数经常被忽略实际开发里要小心。2.3 令牌格式取舍JWT 还是透明令牌拿到 Token 之后还得决定令牌长什么样。Spring Authorization Server 默认支持两种令牌格式Opaque Token不透明令牌和 JWT。Opaque Token 是一串随机字符串校验时必须回到授权服务器查询资源服务器每次请求都要发起一次远程调用JWT 则把用户身份、权限等信息直接编码进令牌里资源服务器只需要用公钥验签完全不需要远程请求。从性能角度考虑JWT 更适应微服务场景。业务服务验签之后直接从令牌里取用户 ID 和权限列表省去一次网络开销。从安全性角度考虑JWT 也有一个隐藏的坑一旦签发在有效期内无法主动让令牌失效。如果你遇到封禁用户、强制下线的需求JWT 会很难办。实际项目里我会分情况处理面向内部服务的接口走 JWT面向外部第三方接入的场景强制要求刷新令牌轮换同时控制访问令牌的有效时间不要太长。JWT 分三段Header 声明签名算法和令牌类型Payload 携带用户信息、权限列表、过期时间Signature 则用私钥对前两段签名。资源服务器拿到令牌后用授权服务器发布的公钥验证签名确认内容没有被篡改。3. 授权服务器搭建全流程3.1 基础依赖与数据表初始化授权服务器需要独立部署我通常把它拆成一个单独的 Spring Boot 工程。依赖方面核心的包是spring-boot-starter-oauth2-authorization-server它会把 Spring Security 的依赖一并引进来。为了让客户端信息和授权记录可持久化还需要 JDBC 相关的依赖数据库用 MySQL 就够了。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-oauth2-authorization-server/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-jdbc/artifactId /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependencySpring Authorization Server 提供了几张默认表分别存储客户端信息、授权记录和用户同意记录表名分别是oauth2_registered_client、oauth2_authorization、oauth2_authorization_consent。你不用自己设计表结构直接从官方源码里拿建表 SQL 即可但要注意字段类型要和你的数据库兼容。建表之后本地开发阶段建议先注册一个内存版客户端方便快速调试。生产环境再把客户端信息迁移到数据库里。注册客户端时需要确定几个核心参数client_id、client_secret、授权模式、回调地址、允许的 scope。这些参数对应数据库oauth2_registered_client表里的一行数据。3.2 客户端注册与授权端点配置Spring Authorization Server 的配置不依赖自动配置需要自己声明核心 Bean。我先把基础配置贴出来这个配置决定了授权服务器的行为模式Configuration EnableWebSecurity public class AuthorizationServerConfig { Bean Order(1) public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { http.securityMatcher(/oauth2/**, /login, /error) .authorizeHttpRequests(authorize - authorize .anyRequest().authenticated()) .formLogin(Customizer.withDefaults()); return http.build(); } Bean public RegisteredClientRepository registeredClientRepository() { RegisteredClient client RegisteredClient.withId(UUID.randomUUID().toString()) .clientId(client-app) .clientSecret({noop}secret) .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) .redirectUri(http://127.0.0.1:8080/login/oauth2/code/client-app) .scope(read) .scope(write) .build(); return new InMemoryRegisteredClientRepository(client); } Bean public AuthorizationServerSettings authorizationServerSettings() { return AuthorizationServerSettings.builder() .issuer(http://localhost:9000) .build(); } }几个容易忽略的点逐个解释。clientSecret前面必须加{noop}前缀否则 Spring Security 的PasswordEncoder会默认用 bcrypt 去比对直接报错真实环境换用{bcrypt}前缀存储加密后的密钥。authorizationServerSettings中的issuer是个容易被忽略但非常重要的配置它会被写进令牌的iss声明里资源服务器校验令牌时会把iss当作一个重要的校验项两边的值对不上会直接验签失败。到这里授权服务器其实已经可以启动并接受授权请求了。启动工程后浏览器访问http://localhost:9000/oauth2/authorize?response_typecodeclient_idclient-appredirect_urihttp://127.0.0.1:8080/login/oauth2/code/client-appscoperead会先跳转到登录页登录后会出现确认授权页面点击同意后浏览器地址栏里会带上?codexxxx这就是授权码。3.3 JWT 签名与自定义声明授权服务器默认生成的令牌是 Opaque Token要生成 JWT必须配置JwtEncoder而JwtEncoder又依赖JWKSource。最稳妥的做法是启动时生成一个 RSA 密钥对公钥通过/oauth2/jwks端点暴露给资源服务器。下面是常用的配置方式Bean public JWKSourceSecurityContext jwkSource() throws NoSuchAlgorithmException { KeyPairGenerator keyPairGenerator KeyPairGenerator.getInstance(RSA); keyPairGenerator.initialize(2048); KeyPair keyPair keyPairGenerator.generateKeyPair(); RSAPublicKey publicKey (RSAPublicKey) keyPair.getPublic(); RSAPrivateKey privateKey (RSAPrivateKey) keyPair.getPrivate(); RSAKey rsaKey new RSAKey.Builder(publicKey) .privateKey(privateKey) .keyID(UUID.randomUUID().toString()) .build(); JWKSet jwkSet new JWKSet(rsaKey); return (jwkSelector, securityContext) - jwkSelector.select(jwkSet); } Bean public JwtDecoder jwtDecoder(JWKSourceSecurityContext jwkSource) { return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); }这里有个小细节如果直接new一个 RSAKey 而不设置 keyID某些 JWT 解析库会因为没有 kid 标识而无法选中正确的密钥。设置一个随机 keyID 是稳妥的做法。生产环境里建议把私钥持久化保存不要让每次重启都重新生成否则之前签发的令牌在重启后就全部失效了。JWT 默认的 claims 只有用户名、scope、过期时间等标准字段。实际业务里资源服务器往往想直接从令牌里拿到当前用户的用户 ID、部门、角色列表这些得通过OAuth2TokenCustomizer塞进去Bean public OAuth2TokenCustomizerJwtEncodingContext tokenCustomizer() { return context - { Authentication principal context.getPrincipal(); if (principal instanceof UsernamePasswordAuthenticationToken) { var user (UserDetails) principal.getPrincipal(); context.getClaims().claim(user_id, user.getUserId()); context.getClaims().claim( authorities, principal.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toList()) ); } }; }自定义声明时要注意一点不要把敏感信息塞进 JWT。令牌是在客户端侧保存的虽然签名可以防篡改但 Payload 本身是 Base64 编码的任何人都能解码看到。手机号、身份证号这种敏感字段最好让资源服务器在拿到 user_id 后通过内部接口去查而不是直接躺在令牌里。3.4 授权码模式完整联调把授权服务器和客户端连起来跑通一次是理解 OAuth2 最有效的办法。这里我用的客户端是一个独立的 Spring Boot 应用它配置了 OAuth2 客户端的自动登录能力。客户端的配置文件和授权服务器的配置对应spring: security: oauth2: client: registration: client-app: client-id: client-app client-secret: secret authorization-grant-type: authorization_code scope: read,write redirect-uri: http://127.0.0.1:8080/login/oauth2/code/client-app provider: client-app: issuer-uri: http://localhost:9000启动客户端应用访问任意受保护的页面Spring Security 会引导到授权服务器的登录页。登录成功并完成授权后客户端应用会收到授权码随后自动请求/oauth2/token换令牌。整个流程可以用 Curl 手动模拟这在排查问题时特别有用# 第一步用授权码换令牌 curl -X POST http://localhost:9000/oauth2/token \ -u client-app:secret \ -H Content-Type: application/x-www-form-urlencoded \ -d grant_typeauthorization_code \ -d redirect_urihttp://127.0.0.1:8080/login/oauth2/code/client-app \ -d codeAUTH_CODE_HERE # 第二步拿到令牌后解析内容 curl http://localhost:9000/oauth2/jwks授权码换令牌这一步-u参数用的是 HTTP Basic 认证把 client_id 和 client_secret 编码在请求头里。请求体里的 redirect_uri 必须和注册时完全一致包括协议、端口、路径一个字符都不能差。授权码是一次性的用了一次之后立刻失效重复使用会报 invalid_grant。4. 资源服务器接入与权限细化4.1 JWT 校验链路的配置资源服务器是业务服务它的任务只有一个验证令牌。Spring Security 6.x 内置了oauth2ResourceServer配置项只需要在任意业务工程里加入依赖和配置即可dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-oauth2-resource-server/artifactId /dependencyspring: security: oauth2: resourceserver: jwt: issuer-uri: http://localhost:9000 jwk-set-uri: http://localhost:9000/oauth2/jwks更推荐的方式是只配置issuer-uriSpring Security 会自动通过发现机制拿到 JWK Set 的地址不需要手动写死。资源服务器的安全过滤链配置如下Configuration EnableWebSecurity public class ResourceServerConfig { Bean public SecurityFilterChain resourceServerSecurityFilterChain(HttpSecurity http) throws Exception { http.securityMatcher(/api/**) .oauth2ResourceServer(oauth2 - oauth2 .jwt(jwt - jwt.decoder(jwtDecoder())) ) .authorizeHttpRequests(authorize - authorize .requestMatchers(/api/public/**).permitAll() .requestMatchers(/api/orders/**).hasAuthority(SCOPE_read) .anyRequest().authenticated() ); return http.build(); } }配置完成后资源服务器收到请求时会有两次关键操作先从 Authorization 请求头里取出 Bearer Token然后用配置好的公钥验签最后把令牌里的 claims 解析成JwtAuthenticationToken放入安全上下文。如果你在控制器里拿到Authentication对象里面的getName()和getAuthorities()就是从令牌里解析出来的身份信息。验签的过程中几处校验特别容易被人忽略。一是令牌是否过期JWT 里的exp声明只要过了时间就会被拒二是iss是否匹配令牌的签发方必须和配置的issuer-uri一致三是对aud的校验如果配置了受众令牌里必须包含对应的受众值。生产环境务必要把这几个默认校验项兜住很多安全漏洞都是因为关闭了某些校验才产生的。4.2 方法级安全与 scope/authority 控制资源服务器除了在过滤链上做粗粒度控制业务逻辑层通常还需要更细粒度的权限判断。比如查询供应商订单时只允许该供应商登录后访问自己名下的订单这种判断不能写死在 URL 匹配里必须在方法上动态判断。Spring Security 的方法级安全注解正好干这个。在 Spring Boot 启动类上加上EnableMethodSecurity就可以用PreAuthorize注解控制方法的访问权限EnableMethodSecurity SpringBootApplication public class ResourceApplication { public static void main(String[] args) { SpringApplication.run(ResourceApplication.class, args); } }RestController RequestMapping(/api/orders) public class OrderController { GetMapping(/{orderId}) PreAuthorize(hasAuthority(SCOPE_read) and #orderId authentication.principal.user_id) public Order getOrder(PathVariable String orderId) { // 业务逻辑 } }注意 scope 和 authority 的区别。OAuth2 的 scope 是客户端申请权限的粒度它在令牌里表现为scope声明Spring Security 会自动把它映射为SCOPE_前缀的权限而自定义 claims 里的权限列表比如我们之前塞进去的authorities则直接映射为hasAuthority(ROLE_ADMIN)这种权限。在 OAuth2 体系里scope 是给第三方应用授权的粗粒度权限自定义 authority 是给用户自身的角色权限两者各司其职。如果混用容易出现权限越界或者无法访问的问题。4.3 浏览器客户端接入细节很多前端项目号称“前后端分离”但接入 OAuth2 时还沿用传统的 Session 方案这是不对的。这种架构下前端把用户引导到授权服务器授权完成后拿到的 Token 应该交给前端保存后续 API 请求由前端在 Authorization 请求头里携带 Token。如果前端是浏览器应用除了授权码模式还可以考虑 BFFBackend for Frontend模式浏览器只和后端网关通信网关负责和授权服务器交互浏览器永远接触不到客户端密钥。网关收到 Token 后加密写入 Cookie后续请求由网关自动附加令牌到转发头里。推荐有一定规模的项目采用这种模式安全性比把 Token 放 LocalStorage 强很多。如果只是做技术验证最简单的联调方式是用 Postman 的 OAuth2 授权工具它能自动完成授权码流程并获取 Token。拿到 Token 后在请求头里手动加上Authorization: Bearer token就能验证资源服务器的接口是否正常响应。5. 实战排查授权链路里的典型故障5.1 授权码换 token 报 invalid_grant这是授权码流程里出现频率最高的问题。一旦看到invalid_grant优先排查这几项授权码是否已经使用过一次性令牌用完即焚redirect_uri 是否完全一致授权码是否已经过期。Spring Authorization Server 默认的授权码有效期只有 5 分钟超过时间找后端要 token 必然失败。除开这些常规原因还有一个隐蔽的坑client_secret 里的特殊字符。如果你在client_secret里用了、、等字符在 POST 请求里没有正确做 URL 编码授权服务器解析时拿到的值就和注册时不一致校验失败就会报 invalid_grant。解决办法是注册时故意用一个不含特殊字符的强密码或者要求前端统一做 encodeURIComponent。还有一次我排查了很久才发现是客户端请求换令牌时用了错误的 Content-Type。Spring Authorization Server 要求必须用application/x-www-form-urlencoded格式提交参数如果客户端误用了application/json即使参数全部正确也会返回 invalid_grant。5.2 资源服务器一直 401问题出在哪里资源服务器返回 401先看日志里是哪种原因。最常见的是Bearer token not found也就是请求头里根本没有携带 Authorization 头。这种情况通常是前端代码只把 Token 存在变量里刷新页面后就丢了或者前端请求库的拦截器没有正确把 Token 附加到请求头。用一个简单的 Curl 命令就能验证curl -H Authorization: Bearer token http://localhost:8080/api/orders/1001如果 Token 明明带了还是 401就要看验签是否失败。验签失败一般源于两类情况一是授权服务器和资源服务器的 JWK 不匹配重启授权服务器时重新生成了密钥对而资源服务器还拿着旧的公钥二是issuer-uri配置不一致。授权服务器上配置的issuer如果带上了内网地址而资源服务器配置的是外网地址那么即使公钥正确也会因为iss不匹配而拒绝。提示排查这类问题时打开授权服务器和资源服务器的 DEBUG 日志过滤org.springframework.security.oauth2包就能看到明确的失败原因。日志里一般会写明“Invalid token”和“signature verification failed”这类关键词比盲猜快得多。5.3 令牌过期与刷新策略JWT 一旦过期客户端就需要用 refresh_token 换新的 access_token。刷新令牌的请求路径和授权码换令牌一致只是 grant_type 不同curl -X POST http://localhost:9000/oauth2/token \ -u client-app:secret \ -H Content-Type: application/x-www-form-urlencoded \ -d grant_typerefresh_token \ -d refresh_tokenREFRESH_TOKEN_VALUE刷新令牌这个环节有两个坑值得注意。一个是刷新令牌的防重放机制Spring Authorization Server 默认采用“刷新令牌轮换”策略每次使用刷新令牌后旧的刷新令牌立即失效并发请求如果同时带着同一个刷新令牌去刷新只有一个会成功。前端如果同时发起了多个请求刷新逻辑需要做并发控制保证同一时间只有一个刷新请求在飞。另外一个坑是令牌更新后的缓存问题。前端拿到新令牌后如果同时有多个正在进行的请求还是用旧令牌后端会直接拒绝。解决方案是让前端请求拦截器在令牌刷新后自动重放那些因 401 失败的请求。很多成熟的前端库已经有现成的 axios 拦截器实现直接参考即可。5.4 CORS 与 OAuth2 的前后端分离注意事项前后端分离部署时资源服务器的接口通常和前端不是同一个域名CORS 问题躲不掉。Spring Security 6.x 的 CORS 配置和之前的版本略有不同需要显式在过滤链中启用Bean public SecurityFilterChain resourceServerSecurityFilterChain(HttpSecurity http) throws Exception { http.cors(Customizer.withDefaults()) .oauth2ResourceServer(oauth2 - oauth2.jwt(Customizer.withDefaults())); return http.build(); } Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config new CorsConfiguration(); config.setAllowedOrigins(List.of(http://localhost:5173)); config.setAllowedMethods(List.of(GET, POST, PUT, DELETE)); config.setAllowedHeaders(List.of(Authorization, Content-Type)); config.setAllowCredentials(false); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/api/**, config); return source; }CORS 配置里有几个容易踩的细节。setAllowCredentials(false)是特意设置的因为当你使用Authorization请求头而不是 Cookie 时其实不需要携带凭证而如果设置成trueallowedOrigins里就不能写*必须明确域名。如果你发现前端请求都发出去了但浏览器一直报 CORS 错误先看看是不是把*和 credentials 同时用了这个组合在浏览器层面是直接禁止的。5.5 Redis 缓存令牌与分布式部署扩展单机部署授权服务器时所有令牌状态都保存在内存里重启即丢失。虽然 JWT 本身是无状态的但刷新令牌、授权码这些需要服务端存储的状态一旦授权服务器重启所有的刷新令牌都会失效所有客户端用户都得重新登录一遍。解决思路是把授权状态存储迁移到外部。Spring Authorization Server 提供了OAuth2AuthorizationService这个接口可以基于 Redis 或数据库实现。官方默认的JdbcOAuth2AuthorizationService已经够用把授权数据持久化到 MySQL 后即使授权服务器重启刷新令牌也依然有效。分布式部署时还有一点要想清楚Spring Authorization Server 的授权码是有状态存储的如果负载均衡把请求分发到不同的实例每个实例必须能够拿到同一个授权码的存储数据。Redis 方案天然适合这个场景如果是 JDBC 方案需要保证数据库是共享的。授权服务器的JWKSource也必须固定不要每个实例生成各自的密钥对否则客户端在实例 A 换的令牌到实例 B 的 JWK 端点验签会失败。生产环境我会单独用一套密钥管理方案统一签发私钥分发给所有实例或者通过配置中心动态注入。写在最后这次扩展做完整个系统的认证授权才算真正闭环授权服务器独立部署负责客户端注册、授权码签发、令牌颁发资源服务器只认 JWT校验完签名后把权限信息交给方法级安全做精细化控制客户端侧则通过标准 OAuth2 流程完成登录和令牌刷新。字段的命名、scope 的粒度、刷新令牌的轮换策略这些都需要根据实际业务反复打磨。有几个自己在实践中沉淀下来的习惯顺便分享给读者授权服务器的issuer在开发环境就要和正式环境的域名对齐切换环境时少踩不少坑JWT 的自定义 claims 尽量精简只放资源服务器真正需要做权限判断的字段日志里永远不要打印完整令牌否则出了问题排查时日志里的令牌比攻击者手里的令牌泄露得还快。Spring Security OAuth2 这套技术栈里的细节远比一篇文章能覆盖的多但先把授权码模式这条主线走通后面做设备授权、客户端凭据、自定义 scope 都会轻松很多。