先别急着抄依赖OAuth2 这套东西在 Spring 生态里有个最大的坑版本断层。我第一次搭授权服务器照着网上的老教程敲EnableAuthorizationServer项目都起不来折腾一下午才搞明白这个注解在 Spring Security 5 之后就被移除了官方把整个 OAuth2 授权服务器单独拆成了一个新项目叫 Spring Authorization Server用起来完全是另一套写法。这篇我把 Spring Cloud Security OAuth2 从概念到落地完整讲一遍基于目前主流的 Spring Security 6 和 Spring Boot 3 来做老项目的迁移路径、新项目的配置方式、还有我踩过的那些 401/403 和令牌验签问题都会写到适合刚接触微服务安全、准备在网关和业务服务里接 OAuth2 的人也适合之前在旧版授权服务器上栽过跟头的同学对照着找原因。1. 内容整体设计与思路拆解1.1 微服务场景下为什么一定要引入 OAuth2单体应用时期的安全模型特别简单登录之后把 session 存在 Tomcat 里浏览器带上 JSESSIONID后端一查就知道你是谁。好使是好使但微服务拆开之后就崩了用户请求先被网关转发到订单服务再转发到用户服务每个服务都有自己的 session用户的登录态根本没法跨服务共享。你当然可以把 session 抽到 Redis 里做成共享但 session 本质上是服务端状态每来一个请求都要查一次存储在横向扩容和服务间调用场景下非常拖后腿。OAuth2 解决这个问题的思路是把认证和鉴权拆开。认证集中交给授权服务器Authorization Server它负责确认你是谁、你能拿到什么权限范围业务服务只做资源服务器Resource Server拿到令牌之后验签、看 scope确认有效就放行完全不依赖共享存储。这样一来订单服务、用户服务、库存服务只需要配置同一套 JWT 验签公钥就能各自独立校验请求身份。Spring Cloud Security 在这里承担的职责更像是一个骨架层它和 Spring Security 配合负责微服务之间的令牌中继、网关安全过滤、服务间调用时的身份传递。真正实现 OAuth2 协议的是 Spring Security 本身但离开了 Spring Cloud Security 提供的网关和微服务集成能力你依然要把令牌透传、下游鉴权这些脏活累活自己写一遍。1.2 2025 年再入门技术选型必须踩在 Spring Security 6 这条线上很多人在选型上的困惑其实是历史包袱。旧方案是 spring-security-oauth2 和 spring-security-oauth2-autoconfigure 这两个依赖里面的EnableAuthorizationServer注解在 Spring Security 5 时代就标记为废弃到 Spring Security 6 之后彻底移除项目也已经停止维护。官方推荐的新方案是独立项目 Spring Authorization ServerSpring Boot 3 里对应的起步依赖是spring-boot-starter-oauth2-authorization-server。Spring Cloud Security 虽然名字里带 cloud但它不提供 OAuth2 协议的实现实际你落地的依赖还是 Spring Security Spring Authorization ServerCloud 层的职责主要是网关令牌中继、安全过滤器链这类分布式的活。所以整个技术栈建议Spring Boot 3.x Spring Cloud 2023.x Spring Security 6.x Spring Authorization Server 1.x这个组合内部兼容性最好踩坑最少。需要特别提醒的是网上一搜Spring Boot OAuth2搜出来的旧教程比例极高。判断一个教程能不能用的标准很简单看它引入的是不是spring-security-oauth2-authorization-server配置方式是不是通过SecurityFilterChain的 HTTP 安全配置来实现而不再是通过自定义配置类加注解。认准这两点你就不会被老代码误导。1.3 授权码模式选定了其他 grant type 先不用管OAuth2 定义了多种授权方式grant type但入门阶段我只建议你把精力放在授权码模式Authorization Code上其他像客户端凭证模式Client Credentials顺手了解就行。为什么因为在 Spring Security 6 里密码模式Password Grant因为安全问题已经被移除了设备授权模式Device Code用得少刷新令牌Refresh Token又是建立在授权码模式之上的进阶能力。授权码模式是所有 Web 应用登录场景的基石——用户点使用某某登录跳到认证页输账号密码授权后跳回来总共两条腿走路理解这一条链路全流程基本就吃透了。2. 核心概念授权流程与令牌机制2.1 四个角色对照代码模块一眼定位OAuth2 里有四个角色资源所有者Resource Owner、客户端Client、授权服务器Authorization Server、资源服务器Resource Server。用生活中的例子来理解资源所有者是你本人授权服务器是小区物业的登记处客户端是你家楼下的快递柜资源服务器是物业给你配的储物间。你想让快递柜替你签收快递让客户端替你访问资源就必须先授权。这本入门里四个角色对应到代码是清清楚楚的授权服务器一个独立的 Spring Boot 服务依赖spring-boot-starter-oauth2-authorization-server负责登录、授权、签发令牌。客户端如果你做的是前后端分离的 Web 应用那你的前端页面角色就是客户端如果你想试 API 联调Postman 也可以充当客户端。资源服务器你的业务微服务比如订单服务、用户服务它们不负责认证只负责校验令牌。资源所有者真实用户最终登录的人。这四个角色在实际工程里可以合并部署比如授权服务器逻辑上独立但物理上可以先合在某个服务里等规模上来了再拆分。不过我不建议入门阶段就做合并独立出一个授权服务资源服务单独接边界最清晰排查问题时也能少死很多脑细胞。2.2 授权码流程完整链路每一步 HTTP 请求都看明白我自己第一次跑通授权码流程时其实对中间跳转的细节是模糊的只知道跳过去再跳回来。后来抓了请求才彻底看明白整个过程其实就四步第一步用户访问客户端应用客户端检测到未登录构造一个跳转 URL 把用户带到授权服务器。这个 URL 大概是这样的http://localhost:9000/oauth2/authorize?response_typecodeclient_idweb-clientredirect_urihttp://127.0.0.1:8080/login/oauth2/code/web-clientscopeopenidstaterandom-state注意response_typecode表明这是授权码模式state参数用于防 CSRF客户端会在回调时校验它和发起时是否一致。第二步授权服务器弹出登录页用户输入账号密码登录。Spring Authorization Server 默认提供登录表单这一步不需要自己写页面。登录成功后服务器会展示是否授权给该应用的确认页面用户点确认后授权服务器生成一个一次性授权码通过 302 重定向回redirect_uri并且把 code 和 state 拼在 URL 上。第三步客户端拿着这个 code在后端直接调用授权服务器的令牌端点换令牌。这里的关键是不能再走浏览器跳转必须由客户端服务端发起请求防止授权码被截获。换令牌的请求长这样curl -X POST http://localhost:9000/oauth2/token \ -H Authorization: Basic $(echo -n web-client:secret | base64) \ -H Content-Type: application/x-www-form-urlencoded \ -d grant_typeauthorization_coderedirect_urihttp://127.0.0.1:8080/login/oauth2/code/web-clientcodeONLY-ONCE-CODEAuthorization: Basic头是客户端自己的凭证redirect_uri必须和第一步申请时的完全一致这一点是安全要求少一个字符都不行。授权码是一次性的换完令牌立即失效重放直接报错。第四步客户端拿到access_token和可选的refresh_token后后续请求在Authorization: Bearer token头里带上它去访问资源服务器。资源服务器拿令牌去授权服务器发布的 JWK 公钥验签验过就算认证通过然后解析 scope 做权限控制。2.3 令牌选型JWT 和 Opaque Token 到底用哪个令牌有两种主流形态JWT 和不透明令牌Opaque Token。JWT 长得像一串以点号分隔的三段乱码三段分别是 Header、Payload、Signature可以 base64 解码看内容但签名部分没法伪造。不透明令牌就是一段随机字符串本身不携带任何信息必须由资源服务器调授权服务器的/oauth2/introspect端点去查询令牌状态才能知道有效性和 scope。JWT 的好处是无状态资源服务器拿到令牌后本地验签即可不需要网络调用来验证响应快、对授权服务器依赖低。坏处是签发之后没法主动吊销只能等它自然过期你改用户角色也不会立刻反映在已签发令牌里。不透明令牌则相反吊销灵动但每次鉴权都要多一次网络消耗。我的建议很直接网关对下游服务转发时用 JWT 做主令牌因为内部网络带宽充足验签成本低无状态的优势在微服务体系里是压倒性的如果业务场景有严格的会话管理需求、需要立即可吊销比如管理员封禁某个用户那就在对外的会话场景里改用不透明令牌或者 JWT 缩短有效期。入门阶段直接在 JWT 上聚焦就够了80% 的场景它都能覆盖。3. 实操从零搭一个授权服务器3.1 依赖引入与工程骨架我建议你新建两个工程来学习一个是授权服务器auth-server端口 9000一个是资源服务器resource-server端口 8080。做实验阶段不要把授权逻辑混进业务服务里拎清了再合不迟。授权服务器pom.xml里核心依赖只有两个一个是 Web一个是授权服务器dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-oauth2-authorization-server/artifactId /dependency这个起步依赖会帮你把 Spring Security、OAuth2 核心、JOSE 支持JWT 签名所需全部带进来不需要额外引入 Spring Security。注意用它引入的 Spring Security 版本是 6.x和 Spring Boot 3.x 是对应的。如果你项目还挂着老的spring-security-oauth2先去掉两个东西的自动配置类会打架报出来的错往往还看不懂。3.2 授权服务器核心配置四个 Bean 一个都不能少新建一个配置类命名为AuthorizationServerConfig。很多教程只贴代码不解释这里我把每个 Bean 的意图讲清楚你以后自己改配置才知道动哪里Configuration public class AuthorizationServerConfig { Bean Order(1) public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { http // 授权服务器的端点走的是 OAuth2 协议不走普通表单登录 .securityMatcher(/oauth2/**, /login/**) .authorizeHttpRequests(authorize - authorize.anyRequest().authenticated()) // 授权服务器默认需要一个登录表单 .formLogin(withDefaults()) // 令牌端点是 POST 请求关闭 CSRF 才能掉通过 .csrf(csrf - csrf.ignoringRequestMatchers(/oauth2/token)) .oauth2ResourceServer(oauth2 - oauth2.jwt(withDefaults())); return http.build(); } Bean Order(2) public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authorize - authorize.anyRequest().authenticated()) .formLogin(withDefaults()); return http.build(); } Bean public RegisteredClientRepository registeredClientRepository() { // 实验阶段用内存实现生产环境换 JDBC RegisteredClient webClient RegisteredClient.withId(UUID.randomUUID().toString()) .clientId(web-client) .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/web-client) .scope(openid) .scope(read) .scope(write) .build(); return new InMemoryRegisteredClientRepository(webClient); } Bean public JWKSourceSecurityContext jwkSource() { // 生产环境一定要持久化 RSA 密钥不然每次重启令牌全部失效 RSAKey rsaKey generateRsaKey(); JWKSet jwkSet new JWKSet(rsaKey); return new ImmutableJWKSet(jwkSet); } Bean public JwtDecoder jwtDecoder(JWKSourceSecurityContext jwkSource) { return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); } private static RSAKey generateRsaKey() { // 实验代码生成一次性的 RSA 密钥对 KeyPair keyPair KeyPairGenerator.getInstance(RSA) .generateKeyPair(); RSAPublicKey publicKey (RSAPublicKey) keyPair.getPublic(); RSAPrivateKey privateKey (RSAPrivateKey) keyPair.getPrivate(); return new RSAKey.Builder(publicKey) .privateKey(privateKey) .keyID(UUID.randomUUID().toString()) .build(); } }配置类里最容易被忽略的是Order。授权服务器有两个过滤器链优先匹配/oauth2/**和/login/**的授权服务器链路剩下所有请求才落到第二条默认链路。如果把优先级写反了OAuth2 端点会被普通安全性逻辑拦截登录流程会陷入死循环授权服务器自己要求先登录而登录页面又被安全拦截器拦着进不去。RegisteredClientRepository定义了谁会来申请令牌。clientSecret我写的是{noop}secret意思是明文密码仅限本地实验。加了{noop}前缀是给 Spring Security 提示不加密真正的生产环境必须用{bcrypt}配合加密字符串后面的常见问题部分我会专门讲这个细节。3.3 把授权服务器跑起来走一遍真实的授权码流程配置写完直接启动auth-server端口 9000。注意默认端口是 8080为了避免和资源服务器冲突在application.yml里加上server: port: 9000接着手动模拟客户端发起授权。先在浏览器打开http://localhost:9000/oauth2/authorize?response_typecodeclient_idweb-clientredirect_urihttp://127.0.0.1:8080/login/oauth2/code/web-clientscopereadstateabc123如果没有登录会先跳到一个默认的登录页。Spring Authorization Server 内置了一套表单登录页你可以随便输入一个注册过的用户。但注意我们没有配置任何用户存储所以这时候根本登录不进去。很多初学者第一次配置就在这里卡住以为是自己写错了其实是缺了用户信息。解决方法是加一个内存用户Bean public UserDetailsService userDetailsService() { UserDetails user User.withUsername(user) .password({noop}password) .roles(USER) .build(); return new InMemoryUserDetailsManager(user); }{noop}同样表示明文实验用。加完这个用户启动后输入 user / password 就能过登录关。登录后页面会提示是否授权给 web-client点是确认浏览器会带着 code 跳到redirect_uri。因为我们的资源服务器还没建这个地址会 404但没关系你可以在跳转失败之前复制地址栏里的 code 参数然后手动执行换令牌的 curl 命令。实际执行中你会遇到一个很有迷惑性的报错invalid_grant。第一反应是 code 过期但其实更大可能是redirect_uri不匹配。Spring Authorization Server 在换取令牌时要求redirect_uri必须和注册的完全一致包括协议、域名、端口、大小写。很多人在授权 URL 里写localhost注册时写127.0.0.1看起来一样其实完全不一样这个我踩过一次印象特别深刻。3.4 解密令牌确认签名和 scope 都对了换到 access_token 之后把它复制到 jwt.io 或直接用 Python 解码token 是两段.分隔把第一段头部和第二段负载做 base64 解码就能看到类似这样的内容{ sub: user, aud: web-client, nbf: 1710000000, scope: [ read ], iss: http://localhost:9000, exp: 1710003600, iat: 1710000000 }aud是令牌的目标客户端scope会影响资源服务器上的权限判断iss是签发者标识资源服务器校验时这个值必须能对上。exp是过期时间默认是access_token5 分钟refresh_token 更长。如果你要调整有效期可以通过自定义OAuth2TokenSettings来配置RegisteredClient webClient RegisteredClient.withId(...) .tokenSettings(TokenSettings.builder() .accessTokenTimeToLive(Duration.ofHours(2)) .refreshTokenTimeToLive(Duration.ofDays(7)) .build()) .build();需要提醒的是iss字段默认会带上端口号。如果资源服务器和授权服务器走不同域名或者做了端口映射issuer不匹配是高频排查点。生产环境建议在授权服务器里显式配置issuer让所有环境保持稳定可用否则每次换个域名部署资源服务器那边的校验就崩了。4. 实操资源服务器接入与网关令牌中继4.1 资源服务器最小配置验签链路两步走新建resource-server工程端口 8080。依赖简单很多只需要 Web 和 Spring Security 的 OAuth2 资源服务器支持dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-oauth2-resource-server/artifactId /dependency配置文件里把授权服务器的 JWK 地址指出来server: port: 8080 spring: security: oauth2: resourceserver: jwt: jwk-set-uri: http://localhost:9000/oauth2/jwks issuer-uri: http://localhost:9000jwk-set-uri是授权服务器的公钥列表地址资源服务器每次验签时如果本地没有缓存会从这个地址拉公钥issuer-uri用于校验 token 里的iss字段。接下来写安全配置Configuration EnableWebSecurity public class ResourceServerConfig { Bean public SecurityFilterChain resourceServerFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authorize - authorize .requestMatchers(/api/public/**).permitAll() .requestMatchers(/api/hello).hasAuthority(SCOPE_read) .anyRequest().authenticated()) .oauth2ResourceServer(oauth2 - oauth2 .jwt(jwt - jwt.jwtAuthenticationConverter(customJwtConverter()))); return http.build(); } private JwtAuthenticationConverter customJwtConverter() { // 默认把 scope 转成 SCOPE_ 前缀的权限这个行为可以在这里定制 JwtAuthenticationConverter converter new JwtAuthenticationConverter(); JwtGrantedAuthoritiesConverter authoritiesConverter new JwtGrantedAuthoritiesConverter(); authoritiesConverter.setAuthorityPrefix(ROLE_); authoritiesConverter.setAuthoritiesClaimName(roles); converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter); return converter; } }默认的 JWT 到权限的转换逻辑是取scope字段然后每一项前面拼上SCOPE_变成SCOPE_read、SCOPE_write。如果你在接口上用hasAuthority(SCOPE_read)判断那就保持默认即可。如果你的授权服务器在签发令牌时把角色放到了自定义字段里比如roles那就要像我上面这样重写转换器把来源字段指过去。这里有一个新手最容易忽略的点permitAll()不等于不校验。Spring Security 的资源服务器链路对所有请求都会先尝试解析 Authorization 头如果请求带了无效令牌配置了permitAll的路径也会返回 401。这不是 Bug而是安全设计——permitAll只是不去做权限判断令牌有效性还是要保证的。如果你真的有匿名可访问但带错令牌也别报 401的需求那需要额外定制AuthenticationEntryPoint这是另一个话题入门阶段先别碰。同一套授权服务器可以对接无数个资源服务器。每个资源服务只认授权服务器的公钥所以只要授权服务器和资源服务器之间网络是通的被拆成多少个微服务不影响整体逻辑。4.2 Spring Cloud Gateway 的令牌中继还是自己手写转发微服务架构里网关是流量的总入口很多人会在网关层做统一的 OAuth2 过滤。Spring Cloud Gateway 提供了TokenRelayGatewayFilterFactory使用起来很简单spring: cloud: gateway: default-filters: - TokenRelayTokenRelay过滤器会自动把原始请求里的 Authorization 头透传给下游服务。看起来很方便但你得有前提网关本身配置了 OAuth2 客户端spring.security.oauth2.client.*并且有一个已经加载到OAuth2AuthorizedClient的会话。换句话说如果你走的是浏览器直接带 token 访问网关的模式TokenRelay反而不起作用因为它依赖的是 OAuth2 客户端的授权对象不是请求头。这是我在业务代码里反复踩过的一个认知误区。所以我的建议是Spring Cloud 微服务场景网关层别过度设计最朴素的方案反而最可靠——网关只做路由和全局过滤器把 Authorization 头原样放行由下游资源服务器各自验签。只有当你们团队确定要走网关统一管理 OAuth2 客户端、页面跳转式登录的 BFF 模式时才值得上TokenRelay。如果只是 API 网关 token 直传手写一个十行的全局过滤器就够了Component public class TokenForwardGlobalFilter implements GlobalFilter, Ordered { Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request exchange.getRequest(); String authorization request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION); if (authorization ! null authorization.startsWith(Bearer )) { ServerHttpRequest mutated request.mutate() .header(HttpHeaders.AUTHORIZATION, authorization) .build(); return chain.filter(exchange.mutate().request(mutated).build()); } return chain.filter(exchange); } Override public int getOrder() { return -100; } }原理本质是换个请求给下游改动量极小排查起来也直观。如果你发现下游永远 401多半不是过滤器问题而是请求头根本没到下游用这个过滤器先确认放行了再说。4.3 scope 的权限模型设计最小授权原则授权服务器签发令牌时带上 scope资源服务器按 scope 做权限判断。这套模型用一句话总结scope 面向客户端权限判断面向业务。一个客户端申请了什么 scope就只能在对应的资源范围内活动。我见过一个普遍不规范的做法把所有接口都放在hasAuthority(SCOPE_read)之下然后所有客户端申请readscope。这样等于没有权限管理任何一个拿到 read 令牌的第三方都可以访问所有数据。规范的做法是把 scope 设计成粒度适中的业务权限比如order.read、order.write、user.read资源服务器按需判断.requestMatchers(HttpMethod.GET, /api/orders/**).hasAuthority(SCOPE_order.read) .requestMatchers(HttpMethod.POST, /api/orders/**).hasAuthority(SCOPE_order.write)如果客户端申请的 scope 没覆盖资源服务器判断时就会因为缺少权限而拒绝。这个拒绝是 403而不是 401这两者的语义差别值得你留意401 是你是谁没确认token 无效或缺失403 是确认了但你没资格token 有效但 scope 不够。排查问题时先分清这两种状态能排除掉一半的干扰项。5. 常见问题与排查技巧实录5.1 所有接口都 401公钥拉不到还是令牌本身不过关资源服务器 401 是最常见的现象我建议按这个顺序排查先确认 Authorization 头是否真的带到了资源服务器网关转发时有没有被吃掉再确认jwk-set-uri能不能从授权服务器拉到公钥手动浏览器打开http://localhost:9000/oauth2/jwks看是否返回 JSON然后检查授权服务器和资源服务器的issuer是否一致重点看端口和域名最后再看令牌有没有过期。这四步走完八成的问题都能找到答案。个我看过无数次的坑是授权服务器改过端口资源服务器配置里的issuer-uri还是旧的。iss是 JWT 里一个硬校验字段资源服务器拿到令牌后会拿iss值和本地配置的issuer-uri做比对不一致直接报Invalid claim: iss。通过 curl 解一个 token 出来看看iss到底是什么往往一眼就能发现问题。5.2 授权码能换但资源服务器验签失败同步时钟JWT 验签失败还有一个隐性问题时钟偏移。exp过期时间和nbf生效时间都是绝对时间戳资源服务器校验时会和本地时间比较。微服务各自部署在不同的机器上如果某台机器时间没同步就会出现在授权服务器上刚换出来的令牌到资源服务器一验就报令牌尚未生效或者已过期。这不是代码问题是运维问题。解决方法是让所有服务统一走 NTP 时间同步。遇到这种明明令牌才签发几秒却报过期的情况别急着怀疑代码先date看一下两边时间差。5.3 老项目迁移没有后悔药但迁移路径很清晰如果你手上是一个还在用EnableAuthorizationServer的老项目好消息是还有一个明确的迁移路径。老依赖spring-security-oauth2已经 EOL不再有任何安全更新继续跑在公网上风险很高我建议尽早迁到 Spring Authorization Server。迁移的核心动作有三个依赖换掉spring-security-oauth2和spring-security-oauth2-autoconfigure整体移除加spring-boot-starter-oauth2-authorization-server配置方式从注解换成SecurityFilterChainRegisteredClientRepository的编码风格令牌存储如果需要持久化把内存实现换成 JDBC 的JdbcRegisteredClientRepository和JdbcOAuth2AuthorizationService。旧客户端配置里的clientSecret如果是 BCrypt 密文可以直接沿用密文值把{bcrypt}前缀加上就行不需要让客户重新改密码。这个细节能帮你省掉大量联调成本。5.4 授权服务器登录页一直循环跳转CSRF 与 Session 的奇怪博弈授权码流程是依赖服务端会话Session的。用户登录后Session 里记录登录态确认授权后跳转回客户端。如果你在授权服务器上配置了csrf.disable()问题不大但如果你用了Order又配错了链路的匹配规则登录后的 Session 没法被正确绑定你会在登录页和授权页之间无限跳来跳去。另一个高频问题是 CSRF token 校验失败。Spring Authorization Server 默认启用了 CSRF 防护如果你用纯 API 或者 SPA 方式接入授权服务器比如前端直接发起授权请求而拿不到 CSRF token就会疯狂 401。对入门场景正确的处理方式不是把 CSRF 关闭而是用官方推荐的OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http)这种默认配置它已经把令牌端点、JWKS 端点等 CSRF 放行规则都处理好了你只需要在它基础上加自己的业务规则千万别全链关掉那是拿安全换一时爽事后必还。5.5 clientSecret 用明文到底行不行实验代码里我用了{noop}secret这是明文前缀能跑但绝对别上生产。Spring Security 的PasswordEncoder机制支持多种前缀{noop}明文、{bcrypt}BCrypt 加密、{sha256}SHA-256 等。如果你在RegisteredClient.clientSecret()里写了{bcrypt}密文Spring 会按 BCrypt 去校验客户端请求中的明文密码。生产环境生成密文很简单用BCryptPasswordEncoder或者在线工具算一个都行但密文一旦入库就别再用{noop}。给客户端配置密钥时还有一个更隐蔽的问题很多团队会把clientSecret写死在代码或 YAML 里一不小心就被提交进 Git 仓库。这不是本入门要展开的 DevOps 内容但至少提醒一句实验环境无所谓生产环境一定要走配置中心或者密钥管理服务。5.6 换了新版本JWT 库冲突怎么办Spring Authorization Server 底层用的是 Nimbus JOSE JWT版本由起步依赖统一管理。你如果再手动引入一个旧版本nimbus-jose-jwt很容易出现NoClassDefFoundError或JWTClaimsSet相关的诡异异常。新项目不要手工加这个库让父工程 BOM 管理即可。老项目从旧版 OAuth2 迁移时也要把之前显式引用的 Nimbus 依赖删掉让新版本自动带。这类冲突的特征是报错信息非常琐碎指向某个内部方法找不到类你看到这种红字先检查依赖冲突往往比查代码更快。6. 基于个人实践的经验补充最后再补充两个我在真实项目里特有体会的细节。第一个是关于授权服务器的密钥持久化。本入门里generateRsaKey()每次启动都生成新密钥意味着授权服务器重启后所有客户端持有的令牌瞬间失效日志清一色 401。这在开发阶段无所谓但生产上每次发布都要全员重新登录体验极差。正确的做法是在启动时从配置中心或环境变量里读取持久化的 RSA 私钥或者用 JDK 自带的KeyStore把密钥对存下来。第二个是监控到位的重要性。授权服务器是全局认证入口它的可用性直接决定所有业务服务的登入体验。至少要有两套环境的告警/oauth2/authorize的响应延迟、/oauth2/jwks的可用性这两条链路断了业务端问都不用问肯定是登录失败。另外想说的是OAuth2 入门最大的障碍从来不是协议本身难懂而是版本信息太乱。如果你在 Spring 生态里做微服务安全建议以后查资料一律锁定 Spring Security 6 和 Spring Authorization Server 的官方文档老博客高赞教程反而要警惕。这篇内容就是按这个思路整理的照着搭一套出来跑通授权码 JWT 验签 网关转发理解链路是怎么闭环的之后再做单点登录、刷新令牌、持久化客户端就是在这个骨架上一个一个加料的事。