You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 06:23:12