
前言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.9SpringSecurity 版本6.4.x随 Boot3.5.9 自动适配JDK17构建工具Maven2.2 pom.xml 完整可运行依赖?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion !-- SpringBoot3.5.9父工程 -- parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.5.9/version relativePath/ /parent groupIdcom.oauth2.demo/groupId artifactIdoauth2-code-demo/artifactId version1.0.0/version nameOAuth2授权码模式实战/name properties java.version17/java.version /properties dependencies !-- SpringMVC Web 基础 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- SpringSecurity6 核心安全框架 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency !-- 【新版核心依赖】官方授权服务器组件 旧版spring-security-oauth2已废弃3.x统一使用该独立包 -- dependency groupIdorg.springframework.security/groupId artifactIdspring-security-oauth2-authorization-server/artifactId /dependency !-- lombok简化代码 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId configuration excludes exclude groupIdorg.projectlombok/groupId artifactIdlombok/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 继承WebSecurityConfigurerAdapter3.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.xEnableAuthorization 彻底移除替换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 { // 新版APIwith(配置器, 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 MapString, Object getUserInfo(Authentication auth) { MapString, 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_typecodeclient_idthird-client-001redirect_urihttps://www.baidu.comscoperead_user_info参数说明response_typecode固定标识授权码模式client_id配置中定义的客户端 IDredirect_uri回调地址必须和配置完全一致scope申请的资源权限操作流程页面跳转 Security 登录页输入账号admin密码123456弹出授权确认弹窗点击 Approve 同意授权自动跳转百度页面URL 末尾携带codexxx一次性授权码仅可使用一次。步骤 2后端 POST 请求换取 access_token请求地址http://localhost:8080/oauth2/token请求方式POST 认证方式Basic Auth Basic 账号third-client-001Basic 密码secret123456表单参数grant_typeauthorization_code code上一步浏览器拿到的code值 redirect_urihttps://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 未授权。步骤 4refresh_token 无感刷新令牌access_token 过期无需重新登录POST 请求刷新接口http://localhost:8080/oauth2/token请求方式POST 认证方式Basic Auth Basic 账号third-client-001Basic 密码secret123456表单参数grant_typerefresh_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令牌过期监控对接日志告警及时感知令牌异常访问。