Spring Boot 2 和 OAuth2/JWT 配置

Posted

技术标签:

【中文标题】Spring Boot 2 和 OAuth2/JWT 配置【英文标题】:Spring Boot 2 and OAuth2/JWT configuration 【发布时间】:2018-01-05 13:44:14 【问题描述】:

我正在尝试将我的 Spring Boot 1.5 应用程序移植到 Spring Boot 2

现在我无法获取 OAuth2 访问令牌。

这是我在 Spring Boot 1.5 中成功使用的代码:

public static String loginAndGetAccessToken(String username, String password, int port) 

    ResourceOwnerPasswordResourceDetails resourceDetails = new ResourceOwnerPasswordResourceDetails();
    resourceDetails.setUsername(username);
    resourceDetails.setPassword(password);
    resourceDetails.setAccessTokenUri(String.format("http://localhost:%d/api/oauth/token", port));
    resourceDetails.setClientId("clientapp");
    resourceDetails.setClientSecret("123456");
    resourceDetails.setGrantType("password");
    resourceDetails.setScope(Arrays.asList("read", "write"));

    DefaultOAuth2ClientContext clientContext = new DefaultOAuth2ClientContext();

    OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(resourceDetails, clientContext);
    restTemplate.setMessageConverters(Arrays.asList(new MappingJackson2HttpMessageConverter()));

    return restTemplate.getAccessToken().toString();

它失败了,但有以下异常:

java.lang.IllegalStateException: An OAuth 2 access token must be obtained or an exception thrown.
    at org.springframework.security.oauth2.client.token.AccessTokenProviderChain.obtainAccessToken(AccessTokenProviderChain.java:124)
    at org.springframework.security.oauth2.client.OAuth2RestTemplate.acquireAccessToken(OAuth2RestTemplate.java:221)
    at org.springframework.security.oauth2.client.OAuth2RestTemplate.getAccessToken(OAuth2RestTemplate.java:173)

看起来http://localhost:%d/api/oauth/token 端点现在是安全的,无法访问

这是我的配置:

OAuth2ServerConfig

@Configuration
public class OAuth2ServerConfig 

    public static final String RESOURCE_ID = "restservice";
    public static final String EXAMPLE_CLIENT_ID = "example_client_id";

    @Value("$jwt.access.token.converter.signing.key")
    private String jwtAccessTokenConverterSigningKey;

    @Value("$jwt.access.token.validity.seconds")
    private int accessTokenValiditySeconds;

    @Autowired
    private UserDetailsService userDetailsService;

    @Bean
    @Primary
    public DefaultTokenServices tokenServices() 
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setTokenStore(tokenStore());
        tokenServices.setSupportRefreshToken(true);
        tokenServices.setAccessTokenValiditySeconds(accessTokenValiditySeconds);
        return tokenServices;
    

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() 
        JwtAccessTokenConverter converter = new UserAwareAccessTokenConverter();

        converter.setSigningKey(jwtAccessTokenConverterSigningKey);

        DefaultAccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter();
        DefaultUserAuthenticationConverter userTokenConverter = new DefaultUserAuthenticationConverter();
        userTokenConverter.setUserDetailsService(userDetailsService);
        accessTokenConverter.setUserTokenConverter(userTokenConverter);

        converter.setAccessTokenConverter(accessTokenConverter);

        return converter;
    

    @Bean
    public TokenStore tokenStore() 
        return new JwtTokenStore(accessTokenConverter());
    

    @Configuration
    @EnableAuthorizationServer
    protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter 

        @Autowired
        @Qualifier("authenticationManagerBean")
        private AuthenticationManager authenticationManager;

        @Value("$jwt.access.token.validity.seconds")
        private int accessTokenValiditySeconds;

        @Autowired
        private TokenStore tokenStore;

        @Autowired
        private TokenEnhancer tokenEnhancer;

        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception 
            // @formatter:off
            endpoints
                .tokenStore(tokenStore)
                .tokenEnhancer(tokenEnhancer)
                .authenticationManager(this.authenticationManager);
            // @formatter:on
        

        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception 
            // @formatter:off
            clients
                .inMemory()
                    .withClient("clientapp")
                        .authorizedGrantTypes("password","refresh_token")
                        .authorities("ROLE_CLIENT")
                        .scopes("read", "write")
                        .resourceIds(RESOURCE_ID)
                        .secret("123456")
                    .and()
                    .withClient(EXAMPLE_CLIENT_ID)
                        .authorizedGrantTypes("implicit")
                        .scopes("read", "write")
                        .autoApprove(true)
                    .and()
                        .withClient("my-trusted-client")
                        .authorizedGrantTypes("password", "authorization_code", "refresh_token", "implicit")
                        .authorities("ROLE_CLIENT", "ROLE_TRUSTED_CLIENT")
                        .scopes("read", "write", "trust")
                        .accessTokenValiditySeconds(accessTokenValiditySeconds);
            // @formatter:on
        

    

    @Configuration
    @EnableResourceServer
    protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter 

        @Autowired
        private ResourceServerTokenServices tokenService;

        @Override
        public void configure(ResourceServerSecurityConfigurer resources) 
            // @formatter:off
            resources           
                .resourceId(RESOURCE_ID)
                .tokenServices(tokenService);
            // @formatter:on
        

        @Override
        public void configure(HttpSecurity http) throws Exception 
            // @formatter:off
            http                
                .antMatcher("/v1.0/**").authorizeRequests()
                .antMatchers("/v1.0/search/**").permitAll()
                .antMatchers("/v1.0/users/**").permitAll()
                .antMatchers("/v1.0/decisions/**").permitAll()
                .antMatchers("/v1.0/votes/**").permitAll()
                .antMatchers("/v1.0/likes/**").permitAll()
                .antMatchers("/v1.0/likeables/**").permitAll()
                .antMatchers("/v1.0/flags/**").permitAll()
                .antMatchers("/v1.0/flagtypes/**").permitAll()
                .antMatchers("/v1.0/flaggables/**").permitAll()
                .antMatchers("/v1.0/comments/**").permitAll()
                .antMatchers("/v1.0/commentables/**").permitAll()
                .antMatchers("/v1.0/subscribables/**").permitAll()
                .antMatchers("/v1.0/favoritables/**").permitAll()
                .antMatchers("/v1.0/import/**").permitAll()
                .antMatchers("/v1.0/tags/**").permitAll()
                .antMatchers("/v1.0/medias/**").permitAll()
                .antMatchers("/swagger**").permitAll()
                .anyRequest().authenticated()
                .and()
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(STATELESS); 
            // @formatter:on
        

    


WebMvcConfig

@Configuration
public class WebMvcConfig implements WebMvcConfigurer 

    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) 
        configurer.enable();
    

    @Override
    public void addViewControllers(ViewControllerRegistry registry) 
        registry.addViewController("/").setViewName("index");
        registry.addViewController("/login").setViewName("login");
    

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) 
        registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");

        registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");

        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
    


WebSecurityConfig

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter 

    @Autowired
    private UserDetailsService userDetailsService;

    @Value("$logout.success.url")
    private String logoutSuccessUrl;

    @Override
    protected void configure(HttpSecurity http) throws Exception 

        // @formatter:off
        http
            .cors()
        .and()
            .csrf().ignoringAntMatchers("/v1.0/**", "/logout")
        .and()
            .authorizeRequests()

            .antMatchers("/oauth/authorize").authenticated()
            //Anyone can access the urls
            .antMatchers("/images/**").permitAll()
            .antMatchers("/signin/**").permitAll()
            .antMatchers("/v1.0/**").permitAll()
            .antMatchers("/auth/**").permitAll()
            .antMatchers("/actuator/health").permitAll()
            .antMatchers("/actuator/**").hasAuthority(Permission.READ_ACTUATOR_DATA)
            .antMatchers("/login").permitAll()
            .anyRequest().authenticated()
        .and()
            .formLogin()
                .loginPage("/login")
                .loginProcessingUrl("/login")
                .failureUrl("/login?error=true")
                .usernameParameter("username")
                .passwordParameter("password")
                .permitAll()
            .and()
                .logout()
                    .logoutUrl("/logout")
                    .logoutSuccessUrl(logoutSuccessUrl)
                    .permitAll();
        // @formatter:on
    

    /**
     * Configures the authentication manager bean which processes authentication requests.
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception 
        auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    

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

    @Bean
    public CorsConfigurationSource corsConfigurationSource() 
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("*"));
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
        configuration.setAllowedHeaders(Arrays.asList("authorization", "content-type", "x-auth-token"));
        configuration.setExposedHeaders(Arrays.asList("x-auth-token"));
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    


为了让它与 Spring Boot 2 一起工作,应该进行哪些更改?

【问题讨论】:

这里有相当多的代码,其中大部分与 OAuth 无关。也少了一些碎片。 完整的最小的样本将使诊断更容易 看起来使用 OAuth2 保护的 API 与 Spring Boot 2 中的 Web 安全配置重叠,这就是我添加它们的原因。顺便说一句 - 这个配置在 Spring Boot 1.5 上运行良好 您在此处显示的内容不适用于任何版本的 Spring Boot,因为它不完整。例如,您尚未共享您的依赖项或任何配置属性。如果您不花时间分享一个最小的完整示例,那么人们不太可能会花时间试图填补空白以帮助您。 我已恢复对 SB1.5.7 的更改,将等到 SB2.0 final 的工作版本 @AndyWilkinson 我为迁移到 Spring Boot 2.0.0.M4 的新尝试创建了一个单独的问题,并在那里提供了 GitHub 测试项目来重现该问题 ***.com/questions/46558921/… 【参考方案1】:

OAuth2ServerConfig

import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultUserAuthenticationConverter;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

import com.decisionwanted.api.security.authentication.UserAwareAccessTokenConverter;
import com.decisionwanted.domain.service.user.social.UserDetailsService;

@Configuration
public class OAuth2ServerConfig 

    public static final String RESOURCE_ID = "restservice";
    public static final String CLIENT_ID = "client_id";

    @Value("$jwt.access.token.converter.signing.key")
    private String jwtAccessTokenConverterSigningKey;

    @Value("$jwt.access.token.validity.seconds")
    private int accessTokenValiditySeconds;

    @Autowired
    private UserDetailsService userDetailsService;

    @Bean
    @Primary
    public DefaultTokenServices tokenServices() 
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setTokenStore(tokenStore());
        tokenServices.setSupportRefreshToken(true);
        tokenServices.setAccessTokenValiditySeconds(accessTokenValiditySeconds);
        return tokenServices;
    

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() 
        JwtAccessTokenConverter converter = new UserAwareAccessTokenConverter();

        converter.setSigningKey(jwtAccessTokenConverterSigningKey);

        DefaultAccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter();
        DefaultUserAuthenticationConverter userTokenConverter = new DefaultUserAuthenticationConverter();
        userTokenConverter.setUserDetailsService(userDetailsService);
        accessTokenConverter.setUserTokenConverter(userTokenConverter);

        converter.setAccessTokenConverter(accessTokenConverter);

        return converter;
    

    @Bean
    public TokenStore tokenStore() 
        return new JwtTokenStore(accessTokenConverter());
    

    @Configuration
    @EnableAuthorizationServer
    protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter 

        @Autowired
        @Qualifier("authenticationManagerBean")
        private AuthenticationManager authenticationManager;

        @Value("$jwt.access.token.validity.seconds")
        private int accessTokenValiditySeconds;

        @Autowired
        private TokenStore tokenStore;

        @Autowired
        private TokenEnhancer tokenEnhancer;

        private PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();

        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception 
            // @formatter:off
            endpoints
                .tokenStore(tokenStore)
                .tokenEnhancer(tokenEnhancer)
                .authenticationManager(this.authenticationManager);
            // @formatter:on
        

        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception 
            // @formatter:off
            clients
                .inMemory()
                    .withClient("clientapp")
                        .authorizedGrantTypes("password","refresh_token")
                        .authorities("ROLE_CLIENT")
                        .scopes("read", "write")
                        .resourceIds(RESOURCE_ID)
                        .secret(passwordEncoder.encode("changeit"))
                    .and()
                    .withClient(CLIENT_ID)
                        .authorizedGrantTypes("implicit")
                        .scopes("read", "write")
                        .autoApprove(true)
                    .and()
                        .withClient("my-trusted-client")
                        .authorizedGrantTypes("password", "authorization_code", "refresh_token", "implicit")
                        .authorities("ROLE_CLIENT", "ROLE_TRUSTED_CLIENT")
                        .scopes("read", "write", "trust")
                        .accessTokenValiditySeconds(accessTokenValiditySeconds);
            // @formatter:on
        

    

    @Configuration
    @EnableResourceServer
    protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter 

        @Autowired
        private ResourceServerTokenServices tokenService;

        @Override
        public void configure(ResourceServerSecurityConfigurer resources) 
            // @formatter:off
            resources           
                .resourceId(RESOURCE_ID)
                .tokenServices(tokenService);
            // @formatter:on
        

        @Override
        public void configure(HttpSecurity http) throws Exception 
            // @formatter:off
            http                
                .antMatcher("/v1.0/**").authorizeRequests()
                .antMatchers("/v1.0/search/**").permitAll()
                .antMatchers("/v1.0/users/**").permitAll()
                .antMatchers("/v1.0/decisiongroups/**").permitAll()
                .antMatchers("/swagger**").permitAll()
                .anyRequest().authenticated()
                .and()
                    .cors()
                .and()
                    .csrf().disable()
                .sessionManagement()
                    .sessionCreationPolicy(STATELESS); 
            // @formatter:on
        

    


WebMvcConfig

@Configuration
public class WebMvcConfig implements WebMvcConfigurer 

    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) 
        configurer.enable();
    

    @Override
    public void addViewControllers(ViewControllerRegistry registry) 
        registry.addViewController("/").setViewName("index");
        registry.addViewController("/login").setViewName("login");
    

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) 
        registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");

        registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");

        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
    


WebSecurityConfig

import java.util.Arrays;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
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.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import com.decisionwanted.domain.model.neo4j.security.permission.Permission;
import com.decisionwanted.domain.service.user.social.UserDetailsService;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter 

    @Autowired
    private UserDetailsService userDetailsService;

    @Value("$logout.success.url")
    private String logoutSuccessUrl;

    PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();

    @Override
    protected void configure(HttpSecurity http) throws Exception 

        // @formatter:off
        http
            .cors()
        .and()
            .csrf().ignoringAntMatchers("/v1.0/**", "/logout")
        .and()
            .authorizeRequests()

            .antMatchers("/oauth/authorize").authenticated()
            //Anyone can access the urls
            .antMatchers("/images/**").permitAll()
            .antMatchers("/signin/**").permitAll()
            .antMatchers("/v1.0/**").permitAll()
            .antMatchers("/auth/**").permitAll()
            .antMatchers("/actuator/health").permitAll()
            .antMatchers("/actuator/**").hasAuthority(Permission.READ_ACTUATOR_DATA)
            .antMatchers("/login").permitAll()
            .anyRequest().authenticated()
        .and()
            .formLogin()
                .loginPage("/login")
                .loginProcessingUrl("/login")
                .failureUrl("/login?error=true")
                .usernameParameter("username")
                .passwordParameter("password")
                .permitAll()
            .and()
                .logout()
                    .logoutUrl("/logout")
                    .logoutSuccessUrl(logoutSuccessUrl)
                    .permitAll();
        // @formatter:on
    

    /**
     * Configures the authentication manager bean which processes authentication requests.
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception 
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
    

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

    @Bean
    public CorsConfigurationSource corsConfigurationSource() 
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("*"));
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
        configuration.setAllowedHeaders(Arrays.asList("authorization", "content-type", "x-auth-token"));
        configuration.setExposedHeaders(Arrays.asList("x-auth-token"));
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    


SecurityTestUtils

import java.util.Arrays;

import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.security.oauth2.client.DefaultOAuth2ClientContext;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.token.grant.password.ResourceOwnerPasswordResourceDetails;

public class SecurityTestUtils 

    public static final String AUTH_HEADER_NAME = "Authorization";
    public static final String AUTH_COOKIE_NAME = "AUTH-TOKEN";

    public static String loginAndGetAccessToken(String username, String password, int port) 

        ResourceOwnerPasswordResourceDetails resourceDetails = new ResourceOwnerPasswordResourceDetails();
        resourceDetails.setUsername(username);
        resourceDetails.setPassword(password);
        resourceDetails.setAccessTokenUri(String.format("http://localhost:%d/api/oauth/token", port));
        resourceDetails.setClientId("clientapp");
        resourceDetails.setClientSecret("changeit");
        resourceDetails.setGrantType("password");
        resourceDetails.setScope(Arrays.asList("read", "write"));

        DefaultOAuth2ClientContext clientContext = new DefaultOAuth2ClientContext();

        OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(resourceDetails, clientContext);
        restTemplate.setMessageConverters(Arrays.asList(new MappingJackson2HttpMessageConverter()));

        return restTemplate.getAccessToken().toString();
    


【讨论】:

【参考方案2】:

尝试在你的 spring 安全配置中允许这个 url /api/oauth/token 我注意到它似乎没有定义如何处理这个。

【讨论】:

以上是关于Spring Boot 2 和 OAuth2/JWT 配置的主要内容,如果未能解决你的问题,请参考以下文章

Spring Boot项目搭建(Spring Boot 2.2.4 + MyBatis + MySql)

Spring Boot 2.5.2 现已推出,同步推出Spring Boot 2.4.8

Spring Boot 2.5.2 现已推出,同步推出Spring Boot 2.4.8

Spring 5.1.13 和 Spring Boot 2.2.3 发布

Spring Boot系列Spring Boot整合持久层

Spring Boot 2.X(十六):应用监控之 Spring Boot Actuator 使用及配置