Spring Boot 3.1.3多租户授权服务器:Client Credentials用户认证问题
Spring Boot 3.1.3迁移后client_credentials模式下UserDetails未加载的解决方法
问题描述
将微服务从Spring Boot 2.7.*迁移至Spring Boot 3.1.3后,使用client_credentials模式时,通过x-www-form-urlencoded传递的username和password未被识别,UserDetails无法加载到上下文环境中,导致无法基于用户信息修改JWT令牌。
现有代码(AuthorizationServer.java)
@Configuration @EnableWebSecurity public class AuthorizationServer { @Bean public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) { return new JdbcRegisteredClientRepository(jdbcTemplate); } @Bean public AuthorizationServerSettings authorizationServerSettings() { return AuthorizationServerSettings.builder().build(); } @Bean SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http) throws Exception { OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer(); http.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated() ) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .csrf(CsrfConfigurer::disable) .formLogin(AbstractHttpConfigurer::disable) .logout(AbstractHttpConfigurer::disable) .addFilterBefore(new MultiTenancyInterceptor(), BasicAuthenticationFilter.class) .httpBasic(withDefaults()) .apply(authorizationServerConfigurer); return http.build(); } @Bean public UserDetailsService userDetailsService() { return new CustomUserDetailsService(); } @Bean public PasswordEncoder passwordEncoder() { return PasswordEncoderFactories.createDelegatingPasswordEncoder(); } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { return authenticationConfiguration.getAuthenticationManager(); } @Bean public AuthenticationProvider authenticationProvider() { DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider(); authenticationProvider.setUserDetailsService(userDetailsService()); authenticationProvider.setPasswordEncoder(passwordEncoder()); return authenticationProvider; } }
期望请求示例
curl --location 'http://localhost:8001/login' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --header 'Authorization: Basic <base64(client_id:client_secret)>' \ --data-urlencode 'grant_type=client_credentials' \ --data-urlencode 'username=<your-username>' \ --data-urlencode 'password=<your-password>'
解决方案
标准client_credentials模式仅验证客户端凭据,不会处理用户的username和password参数。要实现需求,需扩展认证流程,同时验证客户端和用户信息,并确保UserDetails被加载到认证上下文。
步骤1:自定义客户端凭据认证转换器
创建CustomClientCredentialsAuthenticationConverter,从表单参数中提取用户凭据,构建包含用户信息的认证对象:
public class CustomClientCredentialsAuthenticationConverter implements AuthenticationConverter { private final OAuth2ClientCredentialsAuthenticationConverter delegate = new OAuth2ClientCredentialsAuthenticationConverter(); @Override public Authentication convert(HttpServletRequest request) { // 先获取标准的客户端凭据认证对象 Authentication clientAuth = delegate.convert(request); if (clientAuth == null) { return null; } // 提取表单中的username和password String username = request.getParameter(OAuth2ParameterNames.USERNAME); String password = request.getParameter(OAuth2ParameterNames.PASSWORD); if (StringUtils.hasText(username) && StringUtils.hasText(password)) { // 构建用户认证令牌 UsernamePasswordAuthenticationToken userAuth = new UsernamePasswordAuthenticationToken(username, password); // 将客户端认证和用户认证组合,放入上下文 return new OAuth2ClientCredentialsAuthenticationToken( clientAuth.getPrincipal(), clientAuth.getCredentials(), clientAuth.getAuthorities(), Map.of("userAuthentication", userAuth) ); } return clientAuth; } }
步骤2:自定义客户端凭据认证提供者
创建CustomClientCredentialsAuthenticationProvider,同时验证客户端和用户凭据:
public class CustomClientCredentialsAuthenticationProvider extends OAuth2ClientCredentialsAuthenticationProvider { private final AuthenticationManager authenticationManager; public CustomClientCredentialsAuthenticationProvider(RegisteredClientRepository registeredClientRepository, AuthenticationManager authenticationManager) { super(registeredClientRepository); this.authenticationManager = authenticationManager; } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { // 先完成标准的客户端认证 OAuth2ClientCredentialsAuthenticationToken clientAuth = (OAuth2ClientCredentialsAuthenticationToken) super.authenticate(authentication); // 获取用户认证令牌 UsernamePasswordAuthenticationToken userAuth = (UsernamePasswordAuthenticationToken) clientAuth.getAdditionalParameters().get("userAuthentication"); if (userAuth != null) { // 验证用户凭据,加载UserDetails Authentication authenticatedUser = authenticationManager.authenticate(userAuth); // 将用户认证信息放入客户端认证的上下文 return new OAuth2ClientCredentialsAuthenticationToken( clientAuth.getPrincipal(), clientAuth.getCredentials(), AuthorityUtils.combineAuthorities(clientAuth.getAuthorities(), authenticatedUser.getAuthorities()), clientAuth.getAdditionalParameters() ); } return clientAuth; } }
步骤3:修改AuthorizationServer配置
更新authServerSecurityFilterChain方法,替换默认的转换器和认证提供者,并配置JWT生成时使用UserDetails:
@Bean SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http, JdbcTemplate jdbcTemplate, AuthenticationConfiguration authenticationConfiguration) throws Exception { OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer(); // 自定义客户端凭据认证流程 authorizationServerConfigurer.tokenEndpoint(tokenEndpoint -> tokenEndpoint.accessTokenRequestConverter(new CustomClientCredentialsAuthenticationConverter()) .authenticationProvider(new CustomClientCredentialsAuthenticationProvider(registeredClientRepository(jdbcTemplate), authenticationManager(authenticationConfiguration))) ); // 配置JWT生成器,注入UserDetailsService以加载用户信息 authorizationServerConfigurer.jwt(jwt -> jwt.jwtCustomizer(jwtCustomizer()) ); http.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated() ) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .csrf(CsrfConfigurer::disable) .formLogin(AbstractHttpConfigurer::disable) .logout(AbstractHttpConfigurer::disable) .addFilterBefore(new MultiTenancyInterceptor(), BasicAuthenticationFilter.class) .httpBasic(withDefaults()) .apply(authorizationServerConfigurer); return http.build(); } @Bean public OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() { return context -> { // 从认证上下文获取用户信息 Authentication authentication = context.getPrincipal(); if (authentication instanceof OAuth2ClientCredentialsAuthenticationToken clientAuth) { Authentication userAuth = (Authentication) clientAuth.getAdditionalParameters().get("userAuthentication"); if (userAuth != null && userAuth.getPrincipal() instanceof UserDetails userDetails) { // 将UserDetails信息添加到JWT声明中 context.getClaims().claim("username", userDetails.getUsername()); context.getClaims().claim("authorities", userDetails.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toList())); // 其他需要添加的用户信息 } } }; }
关键说明
- 标准
client_credentials模式不包含用户认证逻辑,上述扩展通过自定义转换器和认证提供者,将用户凭据纳入认证流程,确保UserDetails被加载。 - 在JWT自定义器中,可以直接从认证上下文获取已加载的
UserDetails,从而修改JWT令牌内容。 - 确保
CustomUserDetailsService正确实现UserDetailsService接口,能够根据username查询并返回用户信息。
内容的提问来源于stack exchange,提问作者yof4ef
相关产品推荐
相关产品推荐

