refresh_token grant_type error: UserDetailsService is required. But I dont want to specify one

refresh_token grant_type error: UserDetailsService is required. But I dont want to specify one

我正在尝试使用 spring 引导和依赖项创建 Oauth authentication/authorization 服务器 * spring-security-oauth2-autoconfigure * 雨云-jose-jwt

我正在关注 docs.spring.io/spring-security-oauth2-boot/docs/current-SNAPSHOT/reference/htmlsingle/#boot-features-security-oauth2-authorization-server

问题是我不想指定 UserDetailsS​​ervice,因为有关用户帐户的信息位于另一个不公开密码的服务中。该服务只有一个 API,其中输入为 user/pass,输出为用户信息(如果用户 exists/credentials 正确)。

所以我的code/configuration和文档有点偏差

@EnableAuthorizationServer
@Configuration
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

     //injections

   @Override
   public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
       endpoints
               .tokenStore(jwtTokenStore)
               .accessTokenConverter(accessTokenConverter)
               .authenticationManager(authenticationManager);
   }
}

@EnableWebSecurity
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

   //injections

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) {
        authenticationManagerBuilder.authenticationProvider(travelerAuthenticationProvider); //my custom // authentication provider that calls the other service for checking credentials
    }
}

@Component
public class TravelerAuthenticationProvider implements AuthenticationProvider {

    private static final Logger LOGGER = LoggerFactory.getLogger(TravelerAuthenticationProvider.class);

    private OrderTravelerProfileClient travelerProfileClient;

    public TravelerAuthenticationProvider(OrderTravelerProfileClient travelerProfileClient) {
        this.travelerProfileClient = travelerProfileClient;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        if (authentication.getName() == null || (authentication.getCredentials().toString().isEmpty())) {
            return null;
        }
        var username = authentication.getName();
        var password = authentication.getCredentials().toString();
        try {
            travelerProfileClient.authenticate(username, password);
        } catch (Exception e) {
            LOGGER.error("checking traveler {} credentials failed", username, e);
            throw new BadCredentialsException("wrong traveler credentials");
        }
        var authorities = Set.of(new SimpleGrantedAuthority("traveler"));
        var updatedAuthentication = new UsernamePasswordAuthenticationToken(username, password, authorities);
        return updatedAuthentication;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

与 client_credentials 和密码流程相关的一切都有效,但是当我尝试使用 refresh_token 流程时,它会抱怨 UserDetailsService is required。我应该如何在不定义 UserDetailsS​​ervice 并仅依赖我的自定义身份验证提供程序的情况下解决问题?

更新: 显然 refresh_token 流程重新检查了身份验证(凭据),这需要另一个类型为 PreAuthenticatedAuthenticationToken.class 的身份验证提供程序。

所以我像这样创建了一个新的身份验证提供程序:

@Component
public class TravelerRefreshTokenBasedAuthenticationProvider implements AuthenticationProvider {

    private static final Logger LOGGER = LoggerFactory.getLogger(TravelerRefreshTokenBasedAuthenticationProvider.class);


    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        var currentAuthentication = (PreAuthenticatedAuthenticationToken) authentication;
            //.....
        return updatedAuthentication;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(PreAuthenticatedAuthenticationToken.class);
    }
}

并将我的安全配置更新为:

@EnableWebSecurity
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

//injections

    //this bean will be more configured by the method below and it will be used by spring boot
    //for authenticating requests. Its kind of an equivalent to userDetailsService
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) {
        authenticationManagerBuilder.authenticationProvider(travelerUserPassBasedAuthenticationProvider);
        authenticationManagerBuilder.authenticationProvider(travelerRefreshTokenBasedAuthenticationProvider);
    }
}

问题是 spring 在 refresh_token 流程中无法识别我的身份验证提供程序并尝试使用默认提供程序。默认的是尝试使用不存在的 UserDetailsS​​ervice。

我也觉得我不需要创建另一个提供程序,我可以重用以前的提供程序。因为 spring 未能使用我的自定义提供程序的检查是针对 user/pass 的检查;我在以前的身份验证提供程序中所做的。

所以总而言之,直到现在,我觉得我必须以不同的方式将我的自定义提供程序介绍给 spring refresh_token 流与密码流

您的 AuthenticationProvider 实现仅支持 UsernamePasswordAuthenticationToken,用于 username/password 身份验证,而 refresh_token 流程尝试使用 PreAuthenticatedAuthenticationToken 更新身份验证(参见 DefaultTokenServices.java)。

因此您需要为 PreAuthenticatedAuthenticationToken 创建另一个 AuthenticationProvider 并将其添加到 AuthenticationManagerBuilder

更新:

我发现 AuthorizationServerEndpointsConfigurer 会创建一个新的 DefaultTokenServices 实例,如果 none 被赋值,这又会创建一个 PreAuthenticatedAuthenticationProvider 的新实例并执行不使用提供的 AuthenticationManager。为避免这种情况,您可以创建自己的 DefaultTokenServices 实例并将其传递给 AuthorizationServerEndpointsConfigurer:

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
    endpoints
            .tokenStore(jwtTokenStore)
            .accessTokenConverter(accessTokenConverter)
            .tokenEnhancer(accessTokenConverter)
            .authenticationManager(authenticationManager)
            .tokenServices(createTokenServices(endpoints, authenticationManager));
}

private DefaultTokenServices createTokenServices(AuthorizationServerEndpointsConfigurer endpoints, AuthenticationManager authenticationManager) {
    DefaultTokenServices tokenServices = new DefaultTokenServices();
    tokenServices.setSupportRefreshToken(true);
    tokenServices.setTokenStore(endpoints.getTokenStore());
    tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
    tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
    tokenServices.setAuthenticationManager(authenticationManager);
    return tokenServices;
}