前言
OAuth2.0 四大模式中授权码模式 (Authorization Code)是生产唯一标准第三方登录方案,微信 / QQ/GitHub 扫码登录底层均基于该模式。 SpringBoot3.x 完全废弃 SpringBoot2.x 老旧@EnableAuthorizationServer、@EnableResourceServer等废弃注解,官方推出独立spring-security-oauth2-authorization-server组件,底层 API 全部重构。 本文仅聚焦授权码模式,完整实现授权服务器 + 资源服务,包含标准流程流程图,代码可直接复制运行,关键代码标注新旧版本差异、详细注释,适配 JDK17,无废弃 API。
一、授权码模式核心背景与设计思路
1.1 传统方案痛点
直接把账号密码交给第三方会造成密码泄露、权限不可控;简化模式令牌暴露前端存在窃取风险。授权码模式拆分两步:
前端仅获取一次性授权码(code,短期失效)
后端携带客户端密钥 + 授权码换取令牌,密钥仅存服务端,前端无感知,安全性最高。
1.2 四大核心角色
资源所有者(用户):拥有账号、用户数据的终端使用者
客户端(第三方应用):小程序、第三方网站,需要获取用户信息
授权服务器:统一登录、下发 code 和令牌的服务
资源服务器:存放用户头像、昵称等受保护接口
1.3 授权码模式标准流程图
授权码模式标准竖向流程图:
流程文字分步解读:
客户端跳转授权服务,申请授权码;
用户登录并确认授权;
授权服务返回一次性 code 到前端回调地址;
后端服务发起令牌请求(密钥仅后端持有,前端看不到 secret);
授权服务校验,下发短期访问令牌 access_token、长期刷新令牌 refresh_token;
客户端携带 access_token 调用资源接口,获取用户数据。
客户端跳转授权服务,申请授权码;
用户登录并确认授权;
授权服务返回一次性 code 到前端回调地址;
后端服务发起令牌请求(密钥仅后端持有,前端看不到 secret);
授权服务校验,下发短期访问令牌 access_token、长期刷新令牌 refresh_token;
客户端携带 access_token 调用资源接口,获取用户数据。
二、项目环境与完整 Maven 依赖
2.1 环境规范
SpringBoot 版本:3.5.9
SpringSecurity 版本:6.4.x(随 Boot3.5.9 自动适配)
JDK:17+
构建工具:Maven
2.2 pom.xml 完整可运行依赖
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <!-- SpringBoot3.5.9父工程 --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.5.9</version> <relativePath/> </parent> <groupId>com.oauth2.demo</groupId> <artifactId>oauth2-code-demo</artifactId> <version>1.0.0</version> <name>OAuth2授权码模式实战</name> <properties> <java.version>17</java.version> </properties> <dependencies> <!-- SpringMVC Web 基础 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- SpringSecurity6 核心安全框架 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- 【新版核心依赖】官方授权服务器组件 旧版spring-security-oauth2已废弃,3.x统一使用该独立包 --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-authorization-server</artifactId> </dependency> <!-- lombok简化代码 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </configuration> </plugin> </plugin> </plugins> </build> </project>三、全局基础安全配置(Security6 无适配器写法)
版本改动备注:彻底删除
WebSecurityConfigurerAdapter适配器,Security6.x 强制使用函数式编程+Bean注册方式,无任何重写方法,为3.5.9版本唯一合规写法。
package com.oauth2.demo.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; /** * 全局基础安全配置 * 版本差异:SpringBoot2.x 继承WebSecurityConfigurerAdapter,3.x完全废弃该适配器,纯Bean函数式 */ @Configuration @EnableWebSecurity public class GlobalSecurityConfig { /** * 密码加密器 * 新版本强制加密,禁止明文,生产禁止使用NoOpPasswordEncoder */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } /** * 认证管理器,授权码模式、令牌交换流程依赖 * 旧版重写authenticationManagerBean(),新版直接从配置获取 */ @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { return configuration.getAuthenticationManager(); } /** * 全局登录过滤链,处理账号密码登录页面 */ @Bean public SecurityFilterChain globalFilterChain(HttpSecurity http) throws Exception { http // 测试环境关闭CSRF,生产前端项目必须开启 .csrf(csrf -> csrf.disable()) // 放行登录接口,其余需要登录 .authorizeHttpRequests(auth -> auth .requestMatchers("/login").permitAll() .anyRequest().authenticated() ) // 使用Security自带表单登录页面 .formLogin(form -> form.permitAll()); return http.build(); } }注意:该方法仅在需要全局的拦截时使用,并且该方法会覆盖后面建立的拦截器类型,请慎重使用
四、自定义用户业务实现(模拟数据库账号)
package com.oauth2.demo.service; import lombok.RequiredArgsConstructor; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import java.util.List; /** * 用户信息查询服务 * 真实项目替换Mybatis/MybatisPlus数据库查询 */ @Service @RequiredArgsConstructor public class CustomUserDetailService implements UserDetailsService { private final PasswordEncoder passwordEncoder; /** 根据用户名查询用户 */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 模拟数据库用户:账号admin,原始密码123456 if (!"admin".equals(username)) { throw new UsernameNotFoundException("登录用户不存在"); } return new User( username, passwordEncoder.encode("123456"), // 用户权限集合 List.of(new SimpleGrantedAuthority("ROLE_USER"), new SimpleGrantedAuthority("ROLE_ADMIN")) ); } }注意:如果已经实现UserDetailService就不需要再其他地方再通过@bean的方式注入userDetailService,要不然会因为bean冲突导致异常
五、OAuth2 授权服务器核心配置(仅开启授权码模式)
核心改动:1. 废弃 @EnableAuthorizationServer;新版开启授权服务不靠注解,靠注册专用 SecurityFilterChain + 内置配置器实现。2. 客户端构建 API 全部重构;3. 区分独立授权过滤链,不与全局安全链冲突
package com.oauth2.demo.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.EnableOAuth2AuthorizationServer; import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.util.matcher.Request; import org.springframework.security.web.util.matcher.RequestMatcher; import java.util.UUID; /** * OAuth2授权服务器核心配置 * 版本重大改动: * 1. Boot2.x:@EnableAuthorization 彻底移除,替换@EnableOAuth2AuthorizationServer * 2. 旧版ClientDetails废弃,使用RegisteredClient构建第三方客户端 * 3. 授权接口统一为 /oauth2/authorize /oauth2/token * 4. 独立过滤链隔离授权接口,避免和登录逻辑冲突 */ @Configuration // SpringBoot3.x 唯一授权服务开启注解 @EnableOAuth2AuthorizationServer public class OAuth2AuthServerConfig { /** * 注册第三方客户端(内存存储,生产改用JDBC持久化数据库) * 仅配置授权码+刷新令牌两种授权类型,屏蔽密码、客户端凭证模式 */ @Bean public RegisteredClientRepository registeredClientRepository() { Registered client = RegisteredClient.withId(UUID.randomUUID().toString()) // 第三方应用唯一标识 .clientId("third-client-001") // 客户端密钥,{noop}代表未加密,生产必须BCrypt加密 .clientSecret("{noop}secret123456") // 客户端两种认证方式:Basic、表单Post .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST) // 核心:仅开启授权码模式、刷新令牌 .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) // 授权成功回调地址,测试使用百度,业务替换前端域名 .redirectUri("https://www.baidu.com") // 申请权限范围:读取用户信息 .scope("read_user_info") // access_token有效期1小时 .accessTokenValiditySeconds(3600) // refresh_token有效期1天 .refreshTokenValiditySeconds(86400) .build(); return new InMemoryRegisteredClientRepository(client); } @Bean @Order(0) public SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http) throws Exception { // 新版API:with(配置器, lambda自定义),替换废弃的apply() //不允许手动 new 对象,新版本会出现部分组件没有初始化,RegisteredClientRepository读取异常,报client_id参数错误。 OAuth2AuthorizationServerConfigurer configurer = OAuth2AuthorizationServerConfigurer.authorizationServer(); http.with(configurer, Customizer.withDefaults()); // 获取配置器实例,拿到端点匹配器 assert configurer != null; RequestMatcher endpointsMatcher = configurer.getEndpointsMatcher(); http.securityMatcher(endpointsMatcher) .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher)) // 新增:未登录时强制跳转登录地址 .exceptionHandling(ex -> ex.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))); return http.build(); } }需要特殊说明:
- 这个
TokenSettings是客户端维度的令牌有效期,每个 RegisteredClient 可以有不同过期时间。- 全局统一有效期:在
OAuth2AuthorizationServerConfigurer中配置tokenSettings(),优先级低于客户端配置。
六、 跳转登录配置(auth失败,跳转登录页面)
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.DelegatingPasswordEncoder; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; import org.springframework.security.oauth2.server.authorization.settings.TokenSettings; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; import org.springframework.security.web.util.matcher.RequestMatcher; import java.time.Duration; import java.util.HashMap; import java.util.Map; import java.util.UUID; /** * Spring Authorization Server1.5.5 授权服务器核心配置 * 适配版本:SpringBoot3.5.9 + SpringSecurity6.5.7 * !!!新版核心改动点(区别旧版SAS1.0~1.4、Boot2.x) * 1、彻底废弃 @EnableAuthorizationServer 旧注解,唯一启用注解为 @EnableOAuth2AuthorizationServer * 2、完全移除 ClientDetails 旧客户端API,强制使用 RegisteredClient 构建器模式 * 3、SAS1.5.x 优化授权链路校验逻辑,独立授权过滤链,与全局安全链隔离,避免权限覆盖冲突 * 4、标准化接口路径:废弃 /oauth/xxx,统一官方新标准 /oauth2/authorize、/oauth2/token * 5、Security6.5.7 强校验:禁止弱配置、强制规范的客户端认证方式与授权模式 * 功能说明:仅开启生产安全的【授权码模式+刷新令牌模式】,隐式模式、客户端凭证模式等高危模式全部关闭 */ @Configuration // SpringBoot3.x + SAS1.5.x 唯一合法的授权服务器开启注解 @EnableWebSecurity public class LoginPageFilterConfig { //2、登录+错误页面链 @Bean @Order(1) public SecurityFilterChain loginPageFilterChain(HttpSecurity http) throws Exception { http.securityMatcher("/login","/error") .authorizeHttpRequests(auth -> auth.anyRequest().permitAll()) // 必须显式配置登录页面,否则跳转入口失效 该方法默认使用自带的登录页面,如果需要自己写登录页面,使用下面注释的方法,并且需要有对应的页面以及页面的跳转controller .formLogin(form -> form.permitAll()) // .formLogin(form -> form // .loginPage("/login") // .loginProcessingUrl("/doLogin") // .permitAll() // ) .csrf(csrf -> csrf.disable()); return http.build(); } }七、资源服务器配置(校验 access_token,获取用户资源)
改动备注:旧版
@EnableResourceServer注解删除,通过 oauth2Resource 配置开启令牌校验
package com.oauth2.demo.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; import org.springframework.security.web.SecurityFilterChain; /** * OAuth2资源服务配置 * 作用:校验请求携带的JWT令牌,未授权请求返回401 */ @Configuration @EnableWebSecurity public class OAuthResourceConfig { //3、资源服务器JWT校验链,只匹配业务接口 @Bean @Order(2) public SecurityFilterChain resourceServerFilterChain(HttpSecurity http) throws Exception { System.out.println("test complie"); http.securityMatcher("/user/**") .authorizeHttpRequests(auth -> auth .requestMatchers("/guest/hello").permitAll() .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 .jwt(jwt -> jwt.jwtAuthenticationConverter(new JwtAuthenticationConverter())) ) .csrf(csrf -> csrf.disable()); return http.build(); } }八、资源测试 Controller(获取授权后用户信息)
package com.oauth2.demo.controller; import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.HashMap; import java.util.Map; /** * 受保护用户资源接口,必须携带OAuth2 access_token访问 */ @RestController @RequestMapping("/user") public class UserResourceController { @GetMapping("/info") public Map<String, Object> getUserInfo(Authentication auth) { Map<String, Object> res = new HashMap<>(); res.put("username", auth.getName()); res.put("用户权限", auth.getAuthorities()); res.put("提示", "授权码模式校验通过,成功获取用户资源"); return res; } /** 无需令牌的公共游客接口 */ @GetMapping("/guest/hello") public String guest() { return "游客公共接口,不需要OAuth2令牌"; } }九、项目启动类
package com.oauth2.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class OAuthCodeApplication { public static void main(String[] args) { SpringApplication.run(OAuthCodeApplication.class, args); } }十、application.yml 基础配置
server: port: 8080 spring: application: name: oauth2-code-demo十一、完整运行测试步骤(授权码模式全流程)
步骤 1:访问授权页面获取一次性 code
浏览器访问地址:
http://localhost:8080/oauth2/authorize?response_type=code&client_id=third-client-001&redirect_uri=https://www.baidu.com&scope=read_user_info
参数说明:
response_type=code:固定标识授权码模式
client_id:配置中定义的客户端 ID
redirect_uri:回调地址,必须和配置完全一致
scope:申请的资源权限
操作流程:
页面跳转 Security 登录页,输入账号
admin,密码123456;弹出授权确认弹窗,点击 Approve 同意授权;
自动跳转百度页面,URL 末尾携带
code=xxx一次性授权码(仅可使用一次)。
步骤 2:后端 POST 请求换取 access_token
请求地址:http://localhost:8080/oauth2/token请求方式:POST 认证方式:Basic Auth Basic 账号:third-client-001Basic 密码:secret123456表单参数:
grant_type=authorization_code code=上一步浏览器拿到的code值 redirect_uri=https://www.baidu.com
返回标准 JWT 结果:
{ "access_token": "eyJraWQiOiJmZjQwYmNhMy01YzNiLTQ5YjktYjY2MC1lNzg4MTViYmIxYzYiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImF1ZCI6InRoaXJkLWNsaWVudC0wMDEiLCJuYmYiOjE3ODU5NDI1NjUsInNjb3BlIjpbInJlYWRfdXNlcl9pbmZvIl0sImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MCIsImV4cCI6MTc4NTk0NjE2NSwiaWF0IjoxNzg1OTQyNTY1LCJqdGkiOiI4ZmQ2ODE0OC1lZTdlLTQxNGItYThjOC05NTEyNzE5ZDY0Y2MifQ.FcsiaJAE7Ey4OGkjPm0ACo9PPpNUdAmcH2dtmg_HlVaBrvtAXS3_y6k8qPghOfesnkyl_boSFwtV232A88Sad0yGtmU1reQNAwwIxKluWIJ38b-dHMbH7fhKB4wHGxVmCOzMY9L6deUyF17w2NZfBOk0dc-tFsuK6epblnX8b8mj3_XoLLR497SySNdZehVMLo4UhqN13nyEw3GAt1QVHbvLAyF_wWWEYE4omlPFKrIJLljs9lt_dcPeD47kGlWbWbI9nLNaRTR28sQrtT-zWFY_Bo2m_6n30pXKUIHZMEAs8pCAZrVJzK5CrAwmWaYkzuhC-uKF0v2JpRDfw53CjA", "refresh_token": "QbXl5XX_LcFv7lEaKixXQqbeGYv1HDJLUlCwl5b-XqkS8MJnuVR6aD7gGEwjYo7OaKK_MhbfbpPrNVCrROKwEp-zFr9YFPBq897G3AM-33dFkHk0UJRFT8vL9ms7vuW_", "scope": "read_user_info", "token_type": "Bearer", "expires_in": 3600 }步骤 3:携带 Token 访问受保护资源
请求头添加:Authorization: Bearer 你的access_tokenGET 访问:http://localhost:8080/user/info正常返回用户信息;无令牌 / 过期令牌返回 401 未授权。
步骤 4:refresh_token 无感刷新令牌
access_token 过期无需重新登录,POST 请求刷新接口:
http://localhost:8080/oauth2/token
请求方式:POST 认证方式:Basic Auth Basic 账号:third-client-001Basic 密码:secret123456表单参数
grant_type=refresh_token refresh_token=获取到的刷新令牌
十二、新旧版本核心改动汇总
注解体系全面重构旧版:
@EnableAuthorizationServer、@EnableResourceServer全部废弃 新版:统一@EnableOAuth2AuthorizationServer实现授权服务安全配置架构变更Boot2.x:继承
WebSecurityConfigurerAdapter重写方法 Boot3.5.9:适配器类彻底删除,纯 Bean 函数式构建SecurityFilterChain接口路径标准化旧接口
/oauth/authorize、/oauth/token新标准/oauth2/authorize、/oauth2/token客户端存储 API 重构废弃
ClientDetails,使用RegisteredClient构建器定义第三方应用资源服务开启方式旧版独立注解,新版直接在 SecurityFilterChain 链式配置开启 JWT 校验
依赖拆分废弃大而全
spring-security-oauth2,拆分独立授权服务组件,轻量化解耦
十三、生产扩展补充
内存客户端替换 JDBC:接入 MySQL 持久化第三方应用信息;
Redis 存储令牌:解决服务重启令牌失效、分布式多节点共享;
PKCE 拓展:纯前端 SPA 无密钥场景,消除客户端密钥泄露风险;
强制 HTTPS:生产环境所有授权、令牌接口必须 HTTPS 加密传输;
自定义登录页面:替换 Security 默认登录页,对接企业统一登录 UI;
令牌过期监控:对接日志告警,及时感知令牌异常访问。