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

Posted

技术标签:

【中文标题】如何强制 Spring Security 更新 XSRF-TOKEN cookie?【英文标题】:How do I force Spring Security to update XSRF-TOKEN cookie? 【发布时间】:2016-04-28 05:16:11 【问题描述】:

当用户进行身份验证时,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 服务被前端应用程序调用了 3 次,但 /user 服务仅在第一次和第三次调用时返回匹配的 JSESSIONID/XSRF-TOKEN cookie,而不是在第二次调用时返回。

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

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

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

    /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

    对后端 REST /user 服务的第三次调用使用与上面显示的第二个请求中使用的完全相同的用户名和密码凭据,但是对/user 的第三次调用导致XSRF_TOKEN cookie 被正确更新,而相同的正确JSESSIONID 被保留。以下是 FireFox 开发者工具的 Network 选项卡此时显示的内容:

    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 匹配,因此对其他后端休息服务的后续请求现在可以成功。

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

后端/user服务和Security Config的代码在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 错误?

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

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

    WebSecurityConfigurerAdapter,

    OncePerRequestFilter,

    CsrfTokenRepository,

    GlobalAuthenticationConfigurerAdapter 和/或

    Principal 由 /user 服务返回。

但是需要做出哪些具体的改变来解决这个问题呢?

【问题讨论】:

【参考方案1】:

更新答案

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

你应该

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

原答案

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

但是,问题是在执行身份验证之前调用了csrfTokenRepository。这意味着当csrfTokenRepository 检查令牌是否改变了结果,如果为假(它还没有改变)。

要解决此问题,您可以注入自定义 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);
    

【讨论】:

要么将 MyAuthenticationSuccessHandler 设为静态类,要么将其提取到外部文件中(我更喜欢这个) 欢迎把样例发到github,我可以看看 当我将前端代码与后端代码放在同一个 jar 中时,您在此处给出的答案中的更改非常有效。但是现在,在我将前端代码移动到在同一个 devbox 上的不同服务器容器中运行的单独应用程序之后,身份验证不起作用。您的 Spring Security 代码没有更改。当您的相同代码从同一本地主机的其他地方调用时,您是否愿意查看出现的新问题?这是链接:***.com/questions/36487457/…

以上是关于如何强制 Spring Security 更新 XSRF-TOKEN cookie?的主要内容,如果未能解决你的问题,请参考以下文章

强制用户在 Spring Security 中更改过期密码

如何强制 Spring Security OAuth 2 使用 JSON 而不是 XML?

如何在负载均衡器后面强制 Spring Security 发出 https 重定向请求

如何使用xml在spring security中禁用注销确认?

有啥方法可以强制在 spring-security 中的某些页面使用 https 吗?

Spring-Security:忽略服务器名称的别名并强制重新登录