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

Spring Boot应用程序中的REST Spring Security /user服务无法在用户进行身份validation时立即更新XSRF-TOKEN cookie。 这导致/any-other-REST-service-url的下一个请求返回Invalid CSRF certificate错误,直到再次调用/user服务。 如何解决此问题,以便REST /user服务在首先validation用户的同一请求/响应事务中正确更新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服务使用该用户名和密码来validation用户。 但/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 。 这导致不匹配导致对其他rest服务的后续请求中的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" 

更新的xsrf-token现在与jsessionid匹配,因此对其他后端rest服务的后续请求现在可以成功。

可以对下面的代码进行哪些具体更改,以便在首次使用登录表单使用适当的用户名和密码调用/user服务时强制更新XSRF-TOKENJSESSIONID 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] osswheader.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] ossecurity.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] ossecurity.web.csrf.CsrfFilter : Invalid CSRF token found for http://localhost:9000/send-pin 

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

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

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

  1. WebSecurityConfigurerAdapter ,

  2. OncePerRequestFilter ,

  3. CsrfTokenRepository ,

  4. GlobalAuthenticationConfigurerAdapter和/或

  5. /user服务返回的Principal 。

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

更新的答案

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

你应该

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

原始答案

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

但是,问题是在执行身份validation之前调用了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); }