如何强制Spring Security更新XSRF-TOKEN cookie?

时间:2016-01-21 19:25:22

标签: java spring rest spring-mvc spring-security

Spring Boot应用程序中的REST Spring Security /user服务无法在用户进行身份验证时立即更新XSRF-TOKEN cookie。这导致/any-other-REST-service-url的下一个请求返回Invalid CSRF certificate错误,直到再次调用/user服务。如何解决此问题,以便REST /user服务在首先验证用户的同一请求/响应事务中正确更新XSRF-TOKEN cookie?

后端REST /user服务由前端应用程序调用三次,但/user服务仅在第一次和第三次调用时返回匹配的JSESSIONID/XSRF-TOKEN cookie,而不是第二次调用呼叫。

  1. 在对服务器的第一个请求中,没有凭据(没有用户名或密码)发送到/ url模式,我认为这种模式称为/user服务,服务器响应与JSESSIONIDXSRF-TOKEN相关联的匿名用户。 FireFox开发人员工具的“网络”选项卡将这些cookie显示为:

    Response cookies:  
    JSESSIONID:"D89FF3AD2ACA7007D927872C11007BCF"
        path:"/"
        httpOnly:true
    XSRF-TOKEN:"67acdc7f-5127-4ea2-9a7b-831e95957789"
        path:"/"
    

    然后,用户可以无错误地对公共可访问资源发出各种请求,FireFox开发人员工具的“网络”选项卡显示这些相同的Cookie值。

  2. /user服务的第二个请求是通过登录表单完成的,该表单发送有效的用户名和密码,/user服务使用该用户名和密码对用户进行身份验证。但/user服务仅返回更新的jsessionid cookie,并且在此步骤中不更新xsrf-token cookie。以下是此时FireFox开发人员工具的“网络”选项卡中显示的Cookie:

    200 GET user在FireFox的“网络”标签中包含以下Cookie:

    Response cookies:  
    JSESSIONID:"5D3B51A03B9AE218586591E67C53FB89"
        path:"/"
        httpOnly:true
    AUTH1:"yes"
    
    Request cookies:
    JSESSIONID:"D89FF3AD2ACA7007D927872C11007BCF"
    XSRF-TOKEN:"67acdc7f-5127-4ea2-9a7b-831e95957789"
    

    请注意,回复中包含新的JSESSIONID,但未包含新的XSRF-TOKEN。这导致不匹配导致后续对其他休息服务的请求中出现403错误(由于无效的csrf令牌),直到通过对/user服务的第三次调用解决此问题。有没有办法可以强制前面的200 get user同时返回新的XSRF-TOKEN

  3. 对后端REST /user服务的第三次调用使用与上面显示的第二个请求中使用的用户名和密码相同的凭据,但第三次调用/user会导致XSRF_TOKEN Cookie正确更新,同时保留了相同的正确JSESSIONID。以下是此时FireFox开发人员工具的“网络”选项卡显示的内容:

    200 GET user表示不匹配的请求会强制更新响应中的XSRF-TOKEN

    Response cookies:
    XSRF-TOKEN:"ca6e869c-6be2-42df-b7f3-c1dcfbdb0ac7"
        path:"/"
    AUTH1:"yes"
    
    Request cookies:  
    JSESSIONID:"5D3B51A03B9AE218586591E67C53FB89"
    XSRF-TOKEN:"67acdc7f-5127-4ea2-9a7b-831e95957789"
    
  4. 更新的xsrf-token现在与jsessionid匹配,因此对其他后端休息服务的后续请求现在可以成功。

    可以对下面的代码进行哪些具体更改,以便在第一次使用正确的用户名和密码调用XSRF-TOKEN服务时强制更新JSESSIONID/user Cookie通过登录表单?我们是否在Spring中对后端/user方法的代码进行了特定更改?或者是安全配置类中的更改?我们可以尝试解决这个问题?

    后端/user服务和安全配置的代码位于Spring Boot后端应用程序的主应用程序类中,该应用程序位于UiApplication.java中,如下所示:

    @SpringBootApplication
    @Controller
    @EnableJpaRepositories(basePackages = "demo", considerNestedRepositories = true)
    public class UiApplication extends WebMvcConfigurerAdapter {
    
        @Autowired
        private Users users;
    
        @RequestMapping(value = "/{[path:[^\\.]*}")
        public String redirect() {
            // Forward to home page so that route is preserved.
            return "forward:/";
        }
    
        @RequestMapping("/user")
        @ResponseBody
        public Principal user(HttpServletResponse response, HttpSession session, Principal user) {
            response.addCookie(new Cookie("AUTH1", "yes"));
            return user;
        }
    
        public static void main(String[] args) {
            SpringApplication.run(UiApplication.class, args);
        }
    
        @Bean
        public LocaleResolver localeResolver() {
            SessionLocaleResolver slr = new SessionLocaleResolver();
            slr.setDefaultLocale(Locale.US);
            return slr;
        }
    
        @Bean
        public LocaleChangeInterceptor localeChangeInterceptor() {
            LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
            lci.setParamName("lang");
            return lci;
        }
    
        @Override
        public void addViewControllers(ViewControllerRegistry registry) {
            registry.addViewController("/login").setViewName("login");
        }
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(localeChangeInterceptor());
        }
    
        @Order(Ordered.HIGHEST_PRECEDENCE)
        @Configuration
        protected static class AuthenticationSecurity extends GlobalAuthenticationConfigurerAdapter {
    
            @Autowired
            private Users users;
    
            @Override
            public void init(AuthenticationManagerBuilder auth) throws Exception {
                auth.userDetailsService(users);
            }
        }
    
        @SuppressWarnings("deprecation")
        @Configuration
        @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
        @EnableWebMvcSecurity
        @EnableGlobalMethodSecurity(prePostEnabled = true)
        protected static class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    
            @Override
            protected void configure(HttpSecurity http) throws Exception {
                http.httpBasic().and().authorizeRequests()
                    .antMatchers("/registration-form").permitAll()
                    .antMatchers("/confirm-email**").permitAll()
                    .antMatchers("/submit-phone").permitAll()
                    .antMatchers("/check-pin").permitAll()
                    .antMatchers("/send-pin").permitAll()
                    .antMatchers("/index.html", "/", "/login", "/message", "/home", "/public*", "/confirm*", "/register*") 
                    .permitAll().anyRequest().authenticated().and().csrf()
                    .csrfTokenRepository(csrfTokenRepository()).and()
                    .addFilterAfter(csrfHeaderFilter(), CsrfFilter.class);
            }
    
            private Filter csrfHeaderFilter() {
                return new OncePerRequestFilter() {
                    @Override
                    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    
                        CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
                        if (csrf != null) {
                            Cookie cookie = WebUtils.getCookie(request, "XSRF-TOKEN");
                            String token = csrf.getToken();
                            if (cookie == null || token != null && !token.equals(cookie.getValue())) {
                                cookie = new Cookie("XSRF-TOKEN", token);
                                cookie.setPath("/");
                                response.addCookie(cookie);
                            }
                        }
                        filterChain.doFilter(request, response);
                    }
                };
            }
    
            private CsrfTokenRepository csrfTokenRepository() {
                HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
                repository.setHeaderName("X-XSRF-TOKEN");
                return repository;
            }
    
        }
    
    }
    

    显示CSRF错误的服务器日志的相关部分是:

    2016-01-20 02:02:06.811 DEBUG 3995 --- [nio-9000-exec-5] o.s.s.w.header.writers.HstsHeaderWriter  : Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher@70b8c8bb
    2016-01-20 02:02:06.813 DEBUG 3995 --- [nio-9000-exec-5] o.s.security.web.FilterChainProxy        : /send-pin at position 4 of 13 in additional filter chain; firing Filter: 'CsrfFilter'
    2016-01-20 02:02:06.813 DEBUG 3995 --- [nio-9000-exec-5] o.s.security.web.csrf.CsrfFilter         : Invalid CSRF token found for http://localhost:9000/send-pin
    

    我需要对上面的代码进行哪些具体更改才能解决此CSRF错误?

    当后端XSRF服务更改用户的状态(登录,注销等)时,如何强制立即更新/user cookie?

    注意:我猜测(根据我的研究)这个问题的解决方案将涉及更改以下Spring Security类的某些组合的配置,所有这些组合都在下面显示的UiApplication.java中定义:

    1. WebSecurityConfigurerAdapter

    2. OncePerRequestFilter

    3. CsrfTokenRepository

    4. GlobalAuthenticationConfigurerAdapter和/或

    5. /user服务返回的Principal

    6. 但是需要做出哪些具体改变才能解决问题?

1 个答案:

答案 0 :(得分:2)

更新答案

获得401的原因是因为在用户注册时在请求中找到了基本身份验证标头。这意味着Spring Security尝试验证凭据,但用户尚未出现,因此它以401响应。

你应该

  • 使/ register端点公开并提供注册用户的控制器
  • 不要在Authorization标头中包含注册表单的用户名/密码,因为这会导致Spring Security尝试验证凭据。而是将参数作为JSON或表单编码参数包含/注册控制器进程

原始答案

在进行身份验证后,Spring Security使用CsrfAuthenticationStrategy使任何CsrfToken失效(以确保无法进行会话固定攻击)。这就是触发新CsrfToken使用的原因。

但是,问题是在执行身份验证之前调用csrfTokenRepository。这意味着当csrfTokenRepository检查令牌是否已将结果更改为false(它尚未更改)时。

要解决此问题,您可以注入自定义AuthenticationSuccessHandler。例如:

public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    public void onAuthenticationSuccess(HttpServletRequest request,
                HttpServletResponse response, Authentication authentication)
                throws ServletException, IOException {
        CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
        if (csrf != null) {
            Cookie cookie = WebUtils.getCookie(request, "XSRF-TOKEN");
            String token = csrf.getToken();
            if (cookie == null || token != null && !token.equals(cookie.getValue())) {
                cookie = new Cookie("XSRF-TOKEN", token);
                cookie.setPath("/");
                response.addCookie(cookie);
            }
        }
        super.onAuthenticationSuccess(request,response,authentication);
    }
}

然后你可以配置它:

    protected void configure(HttpSecurity http) throws Exception {
        http
            .formLogin()
                .successHandler(new MyAuthenticationSuccessHandler())
                .and()
            .httpBasic().and()
            .authorizeRequests()
                .antMatchers("/registration-form").permitAll()
                .antMatchers("/confirm-email**").permitAll()
                .antMatchers("/submit-phone").permitAll()
                .antMatchers("/check-pin").permitAll()
                .antMatchers("/send-pin").permitAll()
                .antMatchers("/index.html", "/", "/login", "/message", "/home", "/public*", "/confirm*", "/register*").permitAll()
                .anyRequest().authenticated()
                .and()
            .csrf()
                .csrfTokenRepository(csrfTokenRepository())
                .and()
            .addFilterAfter(csrfHeaderFilter(), CsrfFilter.class);
    }