news 2026/9/24 21:24:27

Spring Authorization Server 1.4.0实战:5分钟搞定OAuth2授权码模式与JWT令牌生成

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Authorization Server 1.4.0实战:5分钟搞定OAuth2授权码模式与JWT令牌生成

Spring Authorization Server 1.4.0 实战:从零到一构建企业级OAuth2与JWT认证服务

最近在重构一个微服务架构的权限中心,我面临一个选择:是继续维护那个基于旧版Spring Security OAuth2、代码已经有些臃肿的认证服务,还是拥抱Spring官方在2022年正式推出的Spring Authorization Server?经过一番调研和实际踩坑,我选择了后者。Spring Authorization Server 1.4.0 不仅完全遵循OAuth 2.1和OpenID Connect 1.0规范,更重要的是,它提供了更现代、更灵活的架构设计,让开发者能够真正掌控认证授权的每一个细节。

这篇文章,我想和你分享我如何用Spring Authorization Server 1.4.0,在短时间内搭建起一个功能完备、可扩展性强的认证授权服务。我们不会停留在简单的“Hello World”示例,而是会深入到实际生产环境中可能遇到的配置细节、持久化方案、自定义扩展以及安全最佳实践。无论你是正在评估新的认证方案,还是希望将现有系统迁移到更现代的架构,相信这些实战经验都能给你带来直接的帮助。

1. 项目初始化与环境搭建

开始之前,我们需要明确技术栈。Spring Authorization Server 1.4.0 要求JDK 17或更高版本,并与Spring Boot 3.x系列完美集成。我建议直接使用Spring Boot 3.4.0,它能确保我们获得最新的安全补丁和性能优化。

创建一个新的Spring Boot项目时,除了基础的Web和Security依赖,核心就是引入spring-security-oauth2-authorization-server。你的pom.xmlbuild.gradle文件应该包含以下关键依赖:

<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- 核心:授权服务器 --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-authorization-server</artifactId> <version>1.4.0</version> </dependency> <!-- 用于JWT令牌的编码与解码 --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-jose</artifactId> </dependency> </dependencies>

提示:在实际企业项目中,我强烈建议使用Gradle或Maven的BOM(物料清单)来管理Spring相关依赖的版本,避免潜在的版本冲突。Spring Boot的spring-boot-dependencies已经为我们做了很好的版本协调。

接下来是配置类的编写。这是整个授权服务器的“大脑”,我们需要在这里定义安全过滤器链、客户端信息、用户详情服务以及JWT密钥源。我习惯将配置拆分为几个清晰的Bean,便于维护和理解。

首先,创建一个名为AuthorizationServerConfig的配置类,并用@Configuration@EnableWebSecurity注解标记。核心是配置两个SecurityFilterChain:一个用于处理OAuth2和OpenID Connect的端点(如/oauth2/authorize,/oauth2/token),另一个用于处理默认的Web安全(如登录页面)。

@Configuration @EnableWebSecurity public class AuthorizationServerConfig { @Bean @Order(1) public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer(); http .securityMatcher(authorizationServerConfigurer.getEndpointsMatcher()) .with(authorizationServerConfigurer, Customizer.withDefaults()) .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated()) // 配置当请求需要HTML内容类型时,重定向到登录页 .exceptionHandling(exceptions -> exceptions .defaultAuthenticationEntryPointFor( new LoginUrlAuthenticationEntryPoint("/login"), new MediaTypeRequestMatcher(MediaType.TEXT_HTML) ) ); // 启用OpenID Connect 1.0支持(可选,但推荐) http.getConfigurer(OAuth2AuthorizationServerConfigurer.class) .oidc(Customizer.withDefaults()); return http.build(); } @Bean @Order(2) public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authorize -> authorize .requestMatchers("/css/**", "/js/**").permitAll() // 允许静态资源 .anyRequest().authenticated() ) .formLogin(Customizer.withDefaults()); // 使用默认的登录表单 return http.build(); } }

第一个过滤器链(@Order(1))专门拦截授权服务器的端点。OAuth2AuthorizationServerConfigurer是框架提供的便捷配置器,它会自动设置好/oauth2/authorize/oauth2/token/oauth2/jwks等标准端点。exceptionHandling部分的配置确保了当浏览器访问这些端点且用户未登录时,会被优雅地重定向到登录页面,而不是返回JSON错误。

第二个过滤器链(@Order(2))则处理应用的其他所有请求,比如渲染登录页面本身。这里我们配置了表单登录,并允许对静态资源的访问。

2. 核心组件配置:客户端、用户与JWT

有了安全框架,接下来需要填充内容:谁可以申请令牌(客户端),谁可以登录(用户),以及令牌长什么样(JWT)。

2.1 配置客户端仓库(RegisteredClientRepository)

客户端代表了想要访问用户资源的第三方应用。在开发初期,我们可以使用内存存储,但务必记住,生产环境必须持久化。

@Bean public RegisteredClientRepository registeredClientRepository(PasswordEncoder passwordEncoder) { RegisteredClient publicClient = RegisteredClient.withId(UUID.randomUUID().toString()) .clientId("web-app-client") // 客户端标识 .clientSecret(passwordEncoder.encode("this-is-a-secret-change-in-production!")) // 客户端密钥,务必加密存储 .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) // 支持客户端认证方式 .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) // 授权码模式 .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) // 刷新令牌 .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) // 客户端凭证模式 .redirectUri("http://127.0.0.1:8080/login/oauth2/code/web-app-client") // 授权码回调地址 .redirectUri("https://oidcdebugger.com/debug") // 调试用地址 .scope(OidcScopes.OPENID) // OpenID Connect 范围 .scope(OidcScopes.PROFILE) .scope("read") // 自定义API范围 .scope("write") .tokenSettings(TokenSettings.builder() .accessTokenTimeToLive(Duration.ofHours(1)) // 访问令牌1小时过期 .refreshTokenTimeToLive(Duration.ofDays(7)) // 刷新令牌7天过期 .reuseRefreshTokens(false) // 刷新令牌使用后失效 .authorizationCodeTimeToLive(Duration.ofMinutes(5)) // 授权码5分钟有效 .build()) .clientSettings(ClientSettings.builder() .requireProofKey(false) // 是否要求PKCE(推荐为true以提升移动端安全) .requireAuthorizationConsent(true) // 是否要求用户确认授权 .build()) .build(); return new InMemoryRegisteredClientRepository(publicClient); }

这里有几个关键点需要注意:

  • clientSecret:必须使用PasswordEncoder加密,这里使用了注入的Bean。在生产中,这个密钥应该通过安全的渠道分发和存储。
  • redirectUri:这是安全的关键。必须精确匹配客户端发起授权请求时提供的redirect_uri参数,防止授权码被劫持到恶意站点。
  • TokenSettings:这里定义了令牌的生命周期和行为。reuseRefreshTokens(false)是更安全的选择,意味着每次使用刷新令牌获取新的访问令牌后,旧的刷新令牌会失效。
  • ClientSettingsrequireAuthorizationConsent(true)会让用户在授权时看到一个确认页面,这对于需要明确用户同意的场景很重要。

2.2 配置用户详情服务(UserDetailsService)

用户服务负责验证登录用户的凭证。同样,我们从简单的内存存储开始。

@Bean public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) { UserDetails user = User.withUsername("alice") .password(passwordEncoder.encode("password")) .roles("USER", "ADMIN") // 角色,会转换为 SCOPE_ROLE_ 格式的权限 .build(); UserDetails admin = User.withUsername("bob") .password(passwordEncoder.encode("admin")) .roles("ADMIN") .build(); return new InMemoryUserDetailsManager(user, admin); } @Bean public PasswordEncoder passwordEncoder() { // 使用BCrypt强哈希算法 return new BCryptPasswordEncoder(); }

BCryptPasswordEncoder是目前推荐用于密码哈希的算法,它内置了盐值(salt)处理,能有效抵御彩虹表攻击。

2.3 配置JWT(JSON Web Token)

Spring Authorization Server 默认使用JWT作为不透明令牌(opaque token)的替代,它是一种自包含的令牌格式。我们需要配置一个JWK(JSON Web Key)源,用于对JWT进行签名和验证。

@Bean public JWKSource<SecurityContext> jwkSource() { KeyPair keyPair = generateRsaKey(); // 生成RSA密钥对 RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); RSAKey rsaKey = new RSAKey.Builder(publicKey) .privateKey(privateKey) .keyID(UUID.randomUUID().toString()) // 密钥ID,用于在JWK Set中标识 .build(); JWKSet jwkSet = new JWKSet(rsaKey); return new ImmutableJWKSet<>(jwkSet); } private static KeyPair generateRsaKey() { KeyPair keyPair; try { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); keyPairGenerator.initialize(2048); // 使用2048位密钥长度 keyPair = keyPairGenerator.generateKeyPair(); } catch (Exception ex) { throw new IllegalStateException(ex); } return keyPair; } @Bean public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) { return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); }

generateRsaKey方法在应用启动时生成一个RSA密钥对。在生产环境中,绝对不应该这样做!密钥对必须稳定,否则重启应用后所有之前签发的JWT都将无法验证。你应该从安全的密钥管理系统(如HashiCorp Vault、AWS KMS)或持久化存储中加载密钥。

配置好这些Bean后,一个最基本的授权服务器就启动了。访问http://localhost:8080会跳转到登录页,使用alice/password登录后,你就进入了受保护的应用。

3. 授权码模式实战与测试

OAuth 2.1 明确推荐使用授权码模式(Authorization Code Grant)作为最安全的授权流程,尤其适用于有后端的Web应用。它通过一个中间授权码来交换访问令牌,避免了访问令牌直接暴露给用户浏览器。

让我们来模拟一次完整的授权码流程。

第一步:用户发起授权请求

用户在客户端应用点击“使用XXX登录”,客户端会将用户重定向到授权服务器的授权端点:

GET /oauth2/authorize? response_type=code& client_id=web-app-client& scope=openid%20profile%20read& redirect_uri=http://127.0.0.1:8080/login/oauth2/code/web-app-client& state=some-random-state-string
  • response_type=code:表明这是授权码流程。
  • client_id:我们在RegisteredClient中配置的标识。
  • scope:请求的权限范围,openidprofile是OpenID Connect标准声明的,read是我们自定义的API范围。
  • redirect_uri:必须与注册的URI之一完全匹配。
  • state:一个随机字符串,用于防止CSRF攻击,客户端必须验证返回的state是否与发送的一致。

如果用户未登录,授权服务器会要求用户登录。登录成功后,如果客户端设置了requireAuthorizationConsent(true),用户会看到一个授权确认页面,列出请求的权限范围。用户确认后,授权服务器会将用户重定向回redirect_uri,并附上授权码:

HTTP/1.1 302 Found Location: http://127.0.0.1:8080/login/oauth2/code/web-app-client?code=WYdEaP8a...&state=some-random-state-string

第二步:客户端用授权码交换令牌

客户端后端在收到授权码后,需要向令牌端点发起一个后端到后端的POST请求,用授权码换取访问令牌和ID令牌。

curl -X POST 'http://localhost:8080/oauth2/token' \ -H 'Content-Type: application/x-www-form-urlencoded' \ -H 'Authorization: Basic d2ViLWFwcC1jbGllbnQ6dGhpcy1pcy1hLXNlY3JldC1jaGFuZ2UtaW4tcHJvZHVjdGlvbiE=' \ -d 'grant_type=authorization_code&code=WYdEaP8a...&redirect_uri=http://127.0.0.1:8080/login/oauth2/code/web-app-client'

这里Authorization头使用的是HTTP Basic认证,值是client_id:client_secret的Base64编码。请求体是标准的application/x-www-form-urlencoded格式。

注意:授权码是一次性的,且有效期很短(我们在TokenSettings中设置为5分钟)。成功兑换后,该授权码立即失效。

第三步:解析令牌响应

授权服务器验证通过后,会返回一个JSON响应:

{ "access_token": "eyJhbGciOiJSUzI1NiIs...", "refresh_token": "eyJhbGciOiJSUzI1NiIs...", "id_token": "eyJhbGciOiJSUzI1NiIs...", "token_type": "Bearer", "expires_in": 3600, "scope": "openid profile read" }
  • access_token:用于访问受保护资源的Bearer令牌,这里是JWT格式。
  • refresh_token:用于在访问令牌过期后获取新的访问令牌,而不需要用户再次登录。
  • id_token:OpenID Connect特有的JWT,包含了关于用户身份的信息(如sub-用户标识)。
  • expires_in:访问令牌剩余的秒数。

你可以使用 jwt.io 这样的工具解码access_token,查看其内容(Payload),通常会包含iss(签发者)、sub(用户标识)、aud(受众)、exp(过期时间)、scope(权限范围)等标准声明。

测试要点总结:

测试场景预期结果验证方法
使用错误client_idclient_secret返回401 Unauthorizedinvalid_client错误观察HTTP状态码和错误响应
使用错误或过期的授权码返回400 Bad Requestinvalid_grant错误检查响应体中的error字段
redirect_uri不匹配授权请求阶段即失败,返回错误页面观察授权端点直接返回的错误
请求未注册的scope授权确认页面可能不显示该scope,或令牌中不包含检查返回令牌的scope字段

4. 进阶配置:持久化与生产就绪

内存存储只适用于开发和测试。要让服务具备生产可用性,我们必须解决三个核心数据的持久化:客户端、用户和授权信息(令牌、授权码等)。Spring Authorization Server 为这些场景提供了良好的扩展支持。

4.1 客户端持久化

框架提供了JdbcRegisteredClientRepository,它依赖一组预定义的表结构来存储客户端信息。你需要先在数据库中执行建表SQL(表名通常为oauth2_registered_client),该SQL可以在Spring Authorization Server的源码或文档中找到。

然后,将配置中的RegisteredClientRepositoryBean替换为基于JDBC的实现:

@Bean public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) { // 直接使用框架提供的JDBC实现 return new JdbcRegisteredClientRepository(jdbcTemplate); }

现在,你需要通过管理接口或初始化脚本将客户端信息插入数据库,而不是在代码中写死。表结构包含了客户端的所有配置信息,并以JSON格式存储TokenSettingsClientSettings

如果你有更复杂的需求,比如需要从外部配置中心或API动态加载客户端,可以实现RegisteredClientRepository接口:

@Component public class DynamicRegisteredClientRepository implements RegisteredClientRepository { private final SomeConfigService configService; @Override public void save(RegisteredClient registeredClient) { // 实现保存逻辑,例如写入数据库或调用配置中心API } @Override public RegisteredClient findById(String id) { // 根据ID查找 return loadFromDataSource(id); } @Override public RegisteredClient findByClientId(String clientId) { // 根据client_id查找,这是最常用的方法 return loadFromDataSourceByClientId(clientId); } private RegisteredClient loadFromDataSourceByClientId(String clientId) { // 你的业务逻辑:从数据库、Redis或远程服务加载客户端配置 // 然后将配置转换为RegisteredClient对象 // 这是一个简化的示例 ClientConfig config = configService.getClientConfig(clientId); if (config == null) { return null; } return RegisteredClient.withId(config.getId()) .clientId(config.getClientId()) // ... 设置其他属性 .build(); } }

4.2 用户持久化

对于用户信息,Spring Security 提供了JdbcUserDetailsManager,但它的表结构比较固定。更常见的做法是实现自定义的UserDetailsService,这样可以完全控制用户数据的来源和结构,并能轻松集成现有的用户表。

@Service public class CustomUserDetailsService implements UserDetailsService { private final UserRepository userRepository; // 你的用户数据访问层 private final PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 1. 从数据库查询用户实体 UserEntity userEntity = userRepository.findByUsername(username) .orElseThrow(() -> new UsernameNotFoundException("用户未找到: " + username)); // 2. 查询用户角色/权限(根据你的数据结构) List<SimpleGrantedAuthority> authorities = userRoleRepository.findRolesByUserId(userEntity.getId()) .stream() .map(role -> new SimpleGrantedAuthority("ROLE_" + role.toUpperCase())) .collect(Collectors.toList()); // 3. 构建Spring Security的UserDetails对象 // 注意:这里返回的是Spring Security的内置User类,你也可以返回自定义类实现UserDetails接口 return User.builder() .username(userEntity.getUsername()) .password(userEntity.getPasswordHash()) // 数据库应存储加密后的密码 .authorities(authorities) .accountExpired(!userEntity.isActive()) .credentialsExpired(userEntity.isPasswordExpired()) .disabled(userEntity.isLocked()) .build(); } }

然后在配置类中注入这个自定义的UserDetailsService,Spring Security会自动使用它。

4.3 授权信息持久化

这是保证服务无状态和可扩展的关键。授权信息包括生成的授权码、访问令牌、刷新令牌以及它们与用户、客户端的关联关系。使用JdbcOAuth2AuthorizationService可以轻松实现持久化。

@Bean public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) { return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository); }

同样,你需要先创建对应的数据库表(如oauth2_authorization)。一旦启用,所有授权的状态都将被记录在数据库中。这意味着:

  • 服务器重启后令牌依然有效:只要密钥(JWT签名密钥)不变,之前签发的JWT在有效期内仍可被验证。
  • 支持服务器集群:多台授权服务器实例可以共享同一个数据库,从而水平扩展。
  • 具备管理能力:你可以查询和撤销特定的授权。

4.4 密钥管理

如前所述,在内存中生成RSA密钥对是不可取的。在生产环境中,你应该:

  1. 使用稳定的密钥:通过外部工具(如openssl)生成密钥对,并将私钥安全地存储在环境变量、密钥管理服务或加密的配置文件中。
  2. 支持密钥轮换:定期更换密钥对。Spring Authorization Server 的JWKSource可以配置多个密钥,并指定当前使用的密钥ID (kid)。旧的公钥需要保留一段时间,以便验证之前签发的令牌。
@Bean public JWKSource<SecurityContext> jwkSource() throws Exception { // 从环境变量或配置中心读取私钥PEM字符串 String privateKeyPem = System.getenv("JWT_PRIVATE_KEY"); RSAPrivateKey privateKey = readPrivateKey(privateKeyPem); // 通常从同一来源或通过私钥推导出公钥 RSAPublicKey publicKey = readPublicKey(...); RSAKey rsaKey = new RSAKey.Builder(publicKey) .privateKey(privateKey) .keyID("my-key-id-2024-06") // 使用有意义的、可轮换的key id .build(); JWKSet jwkSet = new JWKSet(rsaKey); return new ImmutableJWKSet<>(jwkSet); }

5. 深度定制:令牌增强与自定义授权模式

Spring Authorization Server 的强大之处在于其高度的可定制性。我们可以轻松地扩展令牌内容,甚至添加全新的授权类型。

5.1 自定义JWT声明(Claims)

默认的JWT包含了标准声明,但业务中我们经常需要添加自定义信息,比如用户所属部门、租户ID、特定权限标签等。这可以通过实现OAuth2TokenCustomizer接口来完成。

@Component public class CustomJwtTokenCustomizer implements OAuth2TokenCustomizer<JwtEncodingContext> { @Override public void customize(JwtEncodingContext context) { // 只在访问令牌和ID令牌中添加自定义声明 if (context.getTokenType().equals(OAuth2TokenType.ACCESS_TOKEN) || context.getTokenType().equals(OAuth2TokenType.ID_TOKEN)) { // 从认证信息中获取Principal,通常是UsernamePasswordAuthenticationToken或OidcUser Authentication principal = context.getPrincipal(); if (principal.getPrincipal() instanceof UserDetails userDetails) { // 假设你的UserDetails实现包含更多信息 if (userDetails instanceof CustomUserDetails customUser) { context.getClaims().claims(claims -> { claims.put("department", customUser.getDepartment()); claims.put("tenant_id", customUser.getTenantId()); // 添加自定义角色或权限,注意避免与标准声明冲突 claims.put("roles", customUser.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toList())); }); } } // 你也可以根据授权类型添加特定声明 if (AuthorizationGrantType.CLIENT_CREDENTIALS.equals(context.getAuthorizationGrantType())) { context.getClaims().claims(claims -> { claims.put("grant_type", "client_credentials"); }); } } } }

这个自定义器会自动被Spring探测并应用到令牌生成过程中。现在,你的JWT负载中就会包含departmenttenant_id等字段,资源服务器可以解析并使用这些信息进行更细粒度的授权控制。

5.2 实现自定义授权类型(如密码模式)

虽然OAuth 2.1不推荐密码模式(Resource Owner Password Credentials),但在某些受信任的内部系统或遗留系统迁移场景中,可能仍有需求。Spring Authorization Server 允许我们添加自定义的授权类型。

实现一个自定义授权类型需要三个核心组件:

  1. 自定义的AuthenticationConverter:用于从HTTP请求中提取认证信息。
  2. 自定义的AuthenticationToken:承载转换后的认证信息。
  3. 自定义的AuthenticationProvider:执行实际的认证逻辑并颁发令牌。

步骤一:定义新的授权类型值

public final class CustomGrantType { public static final AuthorizationGrantType PASSWORD = new AuthorizationGrantType("password"); // 还可以定义其他类型,如 "sms", "wechat" 等 private CustomGrantType() {} }

步骤二:创建认证转换器和令牌

@Component public class PasswordGrantAuthenticationConverter implements AuthenticationConverter { @Override public Authentication convert(HttpServletRequest request) { String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE); if (!CustomGrantType.PASSWORD.getValue().equals(grantType)) { return null; // 不是密码模式,由其他Converter处理 } // 提取客户端认证信息(通常来自Authorization头) Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication(); MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request); String username = parameters.getFirst(OAuth2ParameterNames.USERNAME); String password = parameters.getFirst(OAuth2ParameterNames.PASSWORD); // 参数校验 if (!StringUtils.hasText(username) || !StringUtils.hasText(password)) { throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST); } Map<String, Object> additionalParameters = new HashMap<>(); parameters.forEach((key, value) -> { if (!OAuth2ParameterNames.GRANT_TYPE.equals(key) && !OAuth2ParameterNames.USERNAME.equals(key) && !OAuth2ParameterNames.PASSWORD.equals(key)) { additionalParameters.put(key, value.get(0)); } }); return new PasswordGrantAuthenticationToken(username, password, clientPrincipal, additionalParameters); } } public class PasswordGrantAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken { private final String username; private final String password; public PasswordGrantAuthenticationToken(String username, String password, Authentication clientPrincipal, Map<String, Object> additionalParameters) { super(CustomGrantType.PASSWORD, clientPrincipal, additionalParameters); this.username = username; this.password = password; } // getters ... }

步骤三:创建认证提供者

@Component public class PasswordGrantAuthenticationProvider implements AuthenticationProvider { private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator; private final OAuth2AuthorizationService authorizationService; private final UserDetailsService userDetailsService; private final PasswordEncoder passwordEncoder; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { PasswordGrantAuthenticationToken passwordAuth = (PasswordGrantAuthenticationToken) authentication; // 1. 验证客户端(框架已处理大部分) OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(passwordAuth); RegisteredClient registeredClient = clientPrincipal.getRegisteredClient(); // 2. 验证客户端是否支持此授权类型 if (!registeredClient.getAuthorizationGrantTypes().contains(passwordAuth.getGrantType())) { throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT); } // 3. 验证用户凭证 UserDetails userDetails; try { userDetails = userDetailsService.loadUserByUsername(passwordAuth.getUsername()); } catch (UsernameNotFoundException e) { throw new OAuth2AuthenticationException("invalid_credentials"); } if (!passwordEncoder.matches(passwordAuth.getPassword(), userDetails.getPassword())) { throw new OAuth2AuthenticationException("invalid_credentials"); } // 4. 创建认证主体(代表资源所有者) UsernamePasswordAuthenticationToken userPrincipal = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); // 5. 生成令牌(复用框架的令牌生成逻辑) OAuth2TokenContext tokenContext = DefaultOAuth2TokenContext.builder() .registeredClient(registeredClient) .principal(userPrincipal) // 使用用户主体 .authorizationGrantType(passwordAuth.getGrantType()) .authorizationGrant(passwordAuth) .build(); OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext); // ... 省略错误处理和授权对象保存的代码,与标准流程类似 return new OAuth2AccessTokenAuthenticationToken( registeredClient, clientPrincipal, accessToken, refreshToken, additionalParameters); } @Override public boolean supports(Class<?> authentication) { return PasswordGrantAuthenticationToken.class.isAssignableFrom(authentication); } }

步骤四:注册自定义组件到安全配置

最后,在AuthorizationServerConfig中,将自定义的ConverterProvider注册到/oauth2/token端点。

@Bean @Order(1) public SecurityFilterChain authorizationServerSecurityFilterChain( HttpSecurity http, PasswordGrantAuthenticationConverter passwordConverter, PasswordGrantAuthenticationProvider passwordProvider) throws Exception { OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer(); http .securityMatcher(authorizationServerConfigurer.getEndpointsMatcher()) .with(authorizationServerConfigurer, (authorizationServer) -> authorizationServer .tokenEndpoint(tokenEndpoint -> tokenEndpoint .accessTokenRequestConverter(passwordConverter) // 添加自定义转换器 .authenticationProvider(passwordProvider) // 添加自定义提供者 ) ) // ... 其他配置保持不变 ; return http.build(); }

同时,别忘了在注册客户端时,添加对新授权类型的支持:.authorizationGrantType(CustomGrantType.PASSWORD)

现在,客户端就可以使用密码模式获取令牌了:

curl -X POST 'http://localhost:8080/oauth2/token' \ -H 'Content-Type: application/x-www-form-urlencoded' \ -H 'Authorization: Basic ...' \ -d 'grant_type=password&username=alice&password=password'

通过这种扩展机制,你可以实现诸如短信验证码登录、生物特征认证等任何符合业务需求的授权流程。关键在于理解AuthenticationConverterAuthenticationTokenAuthenticationProvider这三个抽象在Spring Security授权服务器中的协作关系。

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

野火F429开发板实战:CubeMX配置LTDC+DMA2D驱动RGB屏避坑指南

野火F429开发板实战&#xff1a;CubeMX配置LTDCDMA2D驱动RGB屏避坑指南 如果你手头正好有一块野火的STM32F429开发板&#xff0c;并且正打算用它来驱动一块RGB接口的液晶屏&#xff0c;构建一个流畅的图形界面&#xff0c;那么这篇文章可能就是为你准备的。从正点原子或野火的例…

作者头像 李华
网站建设 2026/9/22 4:31:18

晶闸管控制技巧:单相半波整流电路的相位控制深度剖析

晶闸管相位控制实战&#xff1a;从波形解析到系统优化的深度指南 在电力电子领域&#xff0c;单相半波可控整流电路常被视为一个“教学模型”。许多工程师在初次接触后&#xff0c;便因其输出脉动大、变压器利用率低等固有缺陷而将其束之高阁&#xff0c;转向更复杂的全桥或三相…

作者头像 李华
网站建设 2026/9/22 4:43:22

微信小程序横屏适配踩坑记:登录页强制竖屏后如何优雅恢复?

微信小程序横屏适配&#xff1a;从登录页强制竖屏到无缝恢复的实战指南 最近在做一个需要全程横屏展示的微信小程序&#xff0c;本以为在app.json里配个"pageOrientation": "landscape"就万事大吉了。结果在登录环节踩了个大坑&#xff1a;调用微信手机号授…

作者头像 李华