news 2026/9/30 3:05:49

Spring Security 6 + OAuth2 微服务安全实战:授权服务器与资源服务器搭建

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Security 6 + OAuth2 微服务安全实战:授权服务器与资源服务器搭建

先别急着抄依赖,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 Server,Spring Boot 3 里对应的起步依赖是spring-boot-starter-oauth2-authorization-server。Spring Cloud Security 虽然名字里带 cloud,但它不提供 OAuth2 协议的实现,实际你落地的依赖还是 Spring Security + Spring Authorization Server,Cloud 层的职责主要是网关令牌中继、安全过滤器链这类分布式的活。所以整个技术栈建议: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_type=code&client_id=web-client&redirect_uri=http://127.0.0.1:8080/login/oauth2/code/web-client&scope=openid&state=random-state

注意response_type=code表明这是授权码模式,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_type=authorization_code&redirect_uri=http://127.0.0.1:8080/login/oauth2/code/web-client&code=ONLY-ONCE-CODE"

Authorization: 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> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-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 JWKSource<SecurityContext> jwkSource() { // 生产环境一定要持久化 RSA 密钥,不然每次重启令牌全部失效 RSAKey rsaKey = generateRsaKey(); JWKSet jwkSet = new JWKSet(rsaKey); return new ImmutableJWKSet<>(jwkSet); } @Bean public JwtDecoder jwtDecoder(JWKSource<SecurityContext> 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_type=code&client_id=web-client&redirect_uri=http://127.0.0.1:8080/login/oauth2/code/web-client&scope=read&state=abc123

如果没有登录,会先跳到一个默认的登录页。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> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-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:9000

jwk-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: - TokenRelay

TokenRelay过滤器会自动把原始请求里的 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 Mono<Void> 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;配置方式从注解换成SecurityFilterChain+RegisteredClientRepository的编码风格;令牌存储如果需要持久化,把内存实现换成 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 验签 + 网关转发,理解链路是怎么闭环的,之后再做单点登录、刷新令牌、持久化客户端,就是在这个骨架上一个一个加料的事。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 3:05:23

Linux线程控制全攻略:从pthread创建到同步互斥与高并发调优

1. 先说清楚&#xff1a;Linux 线程到底在解决什么问题很多刚接触 Linux 编程的人都会有同一个困惑&#xff1a;我们明明有进程&#xff0c;为什么还要搞线程&#xff1f;我在早期学习的时候也纠结过这个问题。后来用多了才明白&#xff0c;线程并不是凭空冒出来的东西&#xf…

作者头像 李华
网站建设 2026/9/30 3:05:17

校园网课程设计实战:子网划分与VLAN规划完整方案

简介&#xff1a;这是一份面向高校计算机网络课程设计的校园网设计方案&#xff0c;适合计算机、网络工程等专业学生用于课程设计报告撰写、答辩准备或同类项目参考。文档以某高校五个部门、四个C类IP地址为背景&#xff0c;完整呈现了从需求分析、VLAN划分、IP子网规划到三层网…

作者头像 李华
网站建设 2026/9/30 3:05:13

SpringBoot+Vue科研工作量管理系统:环境搭建到二次开发实战

接手过一个SpringBootVue的科研工作量管理系统&#xff0c;从源码结构、数据库脚本到接口文档&#xff0c;完整跑通、二次改造、成功答辩&#xff0c;整个过程踩了不少坑。这类Java Web毕设项目在市面上流传很广&#xff0c;但很多人拿到源码后第一步就卡在环境配置&#xff0c…

作者头像 李华
网站建设 2026/9/30 3:05:12

EtherCAT工业以太网技术解析:从实时原理到多轴同步控制

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 3:04:39

AIGC检测不通过全攻略:从检测原理到降AIGC实操技巧

1. 先搞清楚AIGC检测到底在查什么1.1 为什么"一眼AI"的论文会被系统盯上毕业论文提交后&#xff0c;AIGC检测不通过&#xff0c;几乎是近两年毕业生最崩溃的时刻之一。明明是自己一个字一个字敲出来的&#xff0c;结果系统提示"AIGC检出率高"&#xff0c;甚…

作者头像 李华
网站建设 2026/9/30 3:04:39

计算机网络填空题如何变成协议学习引擎

简介&#xff1a;本资源是一份面向自学考生与高校学生的《计算机网络原理》&#xff08;课程代码04741&#xff09;核心考点复习资料&#xff0c;聚焦填空题专项训练与标准答案解析&#xff0c;助力应试备考与知识查漏补缺。文件为单个Word文档&#xff08;.docx&#xff09;&a…

作者头像 李华