Spring Security OAuth 2 与传统 Spring MVC

Posted

技术标签:

【中文标题】Spring Security OAuth 2 与传统 Spring MVC【英文标题】:Spring Security OAuth 2 with traditional Spring MVC 【发布时间】:2019-01-19 13:53:05 【问题描述】:

为传统的非引导 spring mvc 应用程序设置 spring-security-oauth2(密码授予类型)的问题。

能够检索访问令牌(步骤 1)。

在使用“访问令牌”检索受保护的资源时获取状态 4​​01(参见步骤 2)。

卷曲命令:

    获取访问令牌:

    curl -X POST --user clientapp:123456 http://localhost:8080/oauth/token -H "accept: application/json" -H "content-type: application/x-www-form-urlencoded" -d "grant_type=password&username=adolfo&password =123&scope=read_profile"

回复:

"access_token":"50c7c311-c73a-4a32-bc7e-6801bc64bbe0","token_type":"bearer","expires_in":41545,"scope":"read_profile"

    使用访问令牌检索受保护的数据

    curl -X GET http://localhost:8080/api/profile -H "授权:承载50c7c311-c73a-4a32-bc7e-6801bc64bbe0"

响应:状态 401

The request has not been applied because it lacks valid authentication credentials for the target resource.

配置

//资源服务器配置

 @Configuration
    @EnableResourceServer
    public class OAuth2ResourceServer extends ResourceServerConfigurerAdapter 

        @Override
        public void configure(HttpSecurity http) throws Exception 
            //@formatter:off
            http.authorizeRequests()
                    .anyRequest()
                    .authenticated()
                    .and()
                    .requestMatchers()
                    .antMatchers("/api/**");
            //@formatter:on
        

    

授权服务器配置

 @Configuration
    @EnableAuthorizationServer
    public class OAuth2AuthorizationServer extends AuthorizationServerConfigurerAdapter 


        @Autowired
        private AuthenticationManager authenticationManager;

        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception 
            endpoints.authenticationManager(authenticationManager);
        


        @Override
        public void configure(ClientDetailsServiceConfigurer clients)
                throws Exception 
            clients.inMemory()
                    .withClient("clientapp")
                    .secret("123456")
                    //.autoApprove(true)
                    .redirectUris("http://localhost:9000/callback")
                    .authorizedGrantTypes("password")
                    .scopes("read_profile", "read_contacts");
        


        @Override
        public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception 
            oauthServer.passwordEncoder(NoOpPasswordEncoder.getInstance());
        
    

安全配置

Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter 

@Override
protected void configure(HttpSecurity http) throws Exception 
    http
            .csrf().disable()
            .authorizeRequests()
            .antMatchers("/api/**").permitAll()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .usernameParameter("username")
            .passwordParameter("password")
            .loginPage("/signin")
            .loginProcessingUrl("/authenticate")
            .defaultSuccessUrl("/")
            .failureUrl("/signin-error")
            .permitAll();



@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception 
    return super.authenticationManagerBean();



    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception 
        auth.inMemoryAuthentication().passwordEncoder(NoOpPasswordEncoder.getInstance())
                //.passwordEncoder(passwordEncoder())
                .withUser("adolfo")
                .password("123")
                .roles("USER");
    


//控制器

@Controller
public class UserController 

    @RequestMapping("/api/profile")
    public ResponseEntity<UserProfile> profile() 
        String username = (String) SecurityContextHolder.getContext()
                .getAuthentication().getPrincipal();
        String email = username + "@mailinator.com";
        UserProfile profile = new UserProfile();
        profile.setName(username);
        profile.setEmail(email);
        return ResponseEntity.ok(profile);
    

初始化类

public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer 

     @Override
     protected Class<?>[] getRootConfigClasses() 
     return new Class<?>[] SecurityConfig.class, OAuth2AuthorizationServer.class, OAuth2ResourceServer.class,;
     

    @Override
    protected Class<?>[] getServletConfigClasses() 
        return new Class<?>[]DispatcherConfig.class;
    
    @Override
    protected String[] getServletMappings() 
        return new String[]"/";
    

    @Override
    protected Filter[] getServletFilters() 
        DelegatingFilterProxy delegatingFilterProxy =  new DelegatingFilterProxy("springSecurityFilterChain");
        HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();
        return  new Filter[] delegatingFilterProxy,hiddenHttpMethodFilter;
    


//日志

2018-08-13 15:50:29,333 DEBUG [http-nio-8080-exec-10] o.s.s.w.u.m.OrRequestMatcher [OrRequestMatcher.java:65] Trying to match using Ant [pattern='/oauth/token']
2018-08-13 15:50:29,334 DEBUG [http-nio-8080-exec-10] o.s.s.w.u.m.AntPathRequestMatcher [AntPathRequestMatcher.java:157] Checking match of request : '/api/profile'; against '/oauth/token'
2018-08-13 15:50:29,334 DEBUG [http-nio-8080-exec-10] o.s.s.w.u.m.OrRequestMatcher [OrRequestMatcher.java:65] Trying to match using Ant [pattern='/oauth/token_key']
2018-08-13 15:50:29,334 DEBUG [http-nio-8080-exec-10] o.s.s.w.u.m.AntPathRequestMatcher [AntPathRequestMatcher.java:157] Checking match of request : '/api/profile'; against '/oauth/token_key'
2018-08-13 15:50:29,334 DEBUG [http-nio-8080-exec-10] o.s.s.w.u.m.OrRequestMatcher [OrRequestMatcher.java:65] Trying to match using Ant [pattern='/oauth/check_token']
2018-08-13 15:50:29,334 DEBUG [http-nio-8080-exec-10] o.s.s.w.u.m.AntPathRequestMatcher [AntPathRequestMatcher.java:157] Checking match of request : '/api/profile'; against '/oauth/check_token'
2018-08-13 15:50:29,334 DEBUG [http-nio-8080-exec-10] o.s.s.w.u.m.OrRequestMatcher [OrRequestMatcher.java:72] No matches found
2018-08-13 15:50:29,334 DEBUG [http-nio-8080-exec-10] o.s.s.w.u.m.OrRequestMatcher [OrRequestMatcher.java:65] Trying to match using Ant [pattern='/api/**']
2018-08-13 15:50:29,335 DEBUG [http-nio-8080-exec-10] o.s.s.w.u.m.AntPathRequestMatcher [AntPathRequestMatcher.java:157] Checking match of request : '/api/profile'; against '/api/**'
2018-08-13 15:50:29,335 DEBUG [http-nio-8080-exec-10] o.s.s.w.u.m.OrRequestMatcher [OrRequestMatcher.java:68] matched
2018-08-13 15:50:29,335 DEBUG [http-nio-8080-exec-10] o.s.s.w.FilterChainProxy [FilterChainProxy.java:328] /api/profile at position 1 of 11 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter'
2018-08-13 15:50:29,335 DEBUG [http-nio-8080-exec-10] o.s.s.w.FilterChainProxy [FilterChainProxy.java:328] /api/profile at position 2 of 11 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter'
2018-08-13 15:50:29,335 DEBUG [http-nio-8080-exec-10] o.s.s.w.FilterChainProxy [FilterChainProxy.java:328] /api/profile at position 3 of 11 in additional filter chain; firing Filter: 'HeaderWriterFilter'
2018-08-13 15:50:29,335 DEBUG [http-nio-8080-exec-10] o.s.s.w.FilterChainProxy [FilterChainProxy.java:328] /api/profile at position 4 of 11 in additional filter chain; firing Filter: 'LogoutFilter'
2018-08-13 15:50:29,335 DEBUG [http-nio-8080-exec-10] o.s.s.w.u.m.OrRequestMatcher [OrRequestMatcher.java:65] Trying to match using Ant [pattern='/logout', GET]
2018-08-13 15:50:29,335 DEBUG [http-nio-8080-exec-10] o.s.s.w.u.m.AntPathRequestMatcher [AntPathRequestMatcher.java:157] Checking match of request : '/api/profile'; against '/logout'
2018-08-13 15:50:29,336 DEBUG [http-nio-8080-exec-10] o.s.s.w.u.m.OrRequestMatcher [OrRequestMatcher.java:65] Trying to match using Ant [pattern='/logout', POST]
2018-08-13 15:50:29,336 DEBUG [http-nio-8080-exec-10] o.s.s.w.u.m.AntPathRequestMatcher [AntPathRequestMatcher.java:137] Request 'GET /api/profile' doesn't match 'POST /logout
2018-08-13 15:50:29,336 DEBUG [http-nio-8080-exec-10] o.s.s.w.u.m.OrRequestMatcher [OrRequestMatcher.java:65] Trying to match using Ant [pattern='/logout', PUT]
2018-08-13 15:50:29,336 DEBUG [http-nio-8080-exec-10] o.s.s.w.u.m.AntPathRequestMatcher [AntPathRequestMatcher.java:137] Request 'GET /api/profile' doesn't match 'PUT /logout
2018-08-13 15:50:29,336 DEBUG [http-nio-8080-exec-10] o.s.s.w.u.m.OrRequestMatcher [OrRequestMatcher.java:65] Trying to match using Ant [pattern='/logout', DELETE]
2018-08-13 15:50:29,336 DEBUG [http-nio-8080-exec-10] o.s.s.w.u.m.AntPathRequestMatcher [AntPathRequestMatcher.java:137] Request 'GET /api/profile' doesn't match 'DELETE /logout
2018-08-13 15:50:29,337 DEBUG [http-nio-8080-exec-10] o.s.s.w.u.m.OrRequestMatcher [OrRequestMatcher.java:72] No matches found
2018-08-13 15:50:29,337 DEBUG [http-nio-8080-exec-10] o.s.s.w.FilterChainProxy [FilterChainProxy.java:328] /api/profile at position 5 of 11 in additional filter chain; firing Filter: 'OAuth2AuthenticationProcessingFilter'
2018-08-13 15:50:29,347 DEBUG [http-nio-8080-exec-10] o.s.s.o.p.a.OAuth2AuthenticationProcessingFilter [OAuth2AuthenticationProcessingFilter.java:165] Authentication request failed: error="invalid_token", error_description="Invalid access token: d86d8105-9edf-44dd-94b6-36542cade80f"
2018-08-13 15:50:29,365 DEBUG [http-nio-8080-exec-10] o.s.s.w.h.w.HstsHeaderWriter [HstsHeaderWriter.java:129] Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher@5337f7a
2018-08-13 15:50:29,365 DEBUG [http-nio-8080-exec-10] o.s.s.o.p.e.DefaultOAuth2ExceptionRenderer [DefaultOAuth2ExceptionRenderer.java:101] Written [error="invalid_token", error_description="Invalid access token: d86d8105-9edf-44dd-94b6-36542cade80f"] as "application/json;charset=UTF-8" using [org.springframework.http.converter.json.MappingJackson2HttpMessageConverter@734d0eb7]
2018-08-13 15:50:29,366 DEBUG [http-nio-8080-exec-10] o.s.s.w.c.SecurityContextPersistenceFilter [SecurityContextPersistenceFilter.java:119] SecurityContextHolder now cleared, as request processing completed

查看完整日志here

【问题讨论】:

您的配置似乎没问题,您应该能够使用有效的访问令牌调用控制器方法。要找出问题所在,请尝试调试 OAuth2AuthenticationProcessingFilter。最后你会看到得到401状态码的原因。 @briarheart 从 SecurityConfig 中删除了“@Order”,现在我得到了“invalid_token” 完美!发出新令牌并重试。 重复上述第 1 步和第 2 步,但相同的无效令牌响应。 响应(例如描述)或服务器日志中是否有任何其他信息? 【参考方案1】:

我在您的堆栈跟踪中没有看到 OAuth2AuthenticationProcessingFilter。我认为这是因为 @Order(2) 注释放在 SecurityConfig 类上。您的 web 安全配置与 Spring 的内部配置 (org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfiguration) 冲突。尝试从SecurityConfig 类中删除@Order 注释。

编辑

获得 401 响应的另一个原因是使用 org.springframework.security.oauth2.provider.token.TokenStore 的不同实例:一个用于存储新创建的令牌,另一个用于身份验证。这可能是由于在实现org.springframework.web.servlet.config.annotation.WebMvcConfigurer 引用(例如通过ComponentScan 注释)对已声明为根配置类的其他配置时,servlet 初始化程序的配置错误造成的。它会导致创建两个相互冲突的应用程序上下文。

假设你有三个配置类:

    ServletConfig 实现了org.springframework.web.servlet.config.annotation.WebMvcConfigurerAuthorizationServerConfig; ResourceServerConfig

有两种方法可以配置您的 servlet 初始化程序。第一个是从getServletConfigClasses方法返回ServletConfig类:

public class MyServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer 
    @Override
    protected Class<?>[] getRootConfigClasses() 
        return new Class<?>[] AuthorizationServerConfig.class, AuthorizationServerConfig.class;
    

    @Override
    protected Class<?>[] getServletConfigClasses() 
        return new Class<?>[] ServletConfig.class;
    

通过选择这种方式,您应该确保不将根配置中的类包含到 servlet 配置中(不要对根配置类所在的包从 ServletConfig 类运行组件扫描)。

第二种方法是将ServletConfig移动到根配置类,从getServletConfigClasses返回null

public class MyServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer 
    @Override
    protected Class<?>[] getRootConfigClasses() 
        return new Class<?>[] ServletConfig.class, AuthorizationServerConfig.class, AuthorizationServerConfig.class;
    

    @Override
    protected Class<?>[] getServletConfigClasses() 
        return null;
    

只要您不需要多个调度程序 servlet,就可以选择第二种方式。

【讨论】:

以上是关于Spring Security OAuth 2 与传统 Spring MVC的主要内容,如果未能解决你的问题,请参考以下文章

Spring Security OAuth 2 与传统 Spring MVC

spring-security-oauth2.0 SSO大体流程图

Spring Security 5 OAuth 2.0 ResourceServer 如何与 AuthorizationServer 通信?

将 spring-security-oauth2 授权服务器与 child 和 JWKS 一起使用?

Spring Security---Oauth2详解

Spring Security 与 OAuth2(介绍)