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

标签 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,而不在第二个调用上返回。


在对服务器的第一个请求中,没有凭据(没有用户名或密码)发送到/ url模式,我认为这称为/user服务,并且服务器以JSESSIONIDXSRF-TOKEN响应它与一个匿名用户相关联。 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令牌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开发人员工具此时显示的“网络”选项卡:

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

可以对下面的代码进行哪些特定更改,以在首次使用登录名通过适当的用户名和密码调用XSRF-TOKEN服务时强制更新JSESSIONID/user 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错误,我需要对上面的代码进行哪些特定更改?

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

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


WebSecurityConfigurerAdapter
OncePerRequestFilter
CsrfTokenRepository
GlobalAuthenticationConfigurerAdapter和/或
/user服务返回的Principal


但是,要解决该问题需要进行哪些具体更改?

最佳答案

更新的答案

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

你应该


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


原始答案

认证后,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);
    }

关于java - 如何强制Spring Security更新XSRF-TOKEN cookie?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/34932581/

相关文章:

java - 将输出记录到数据库中

java - 如何使用 REST 调用返回 List<MyObject>?

java - 如何使用 java Rest 服务捕获 Angular 2 项目中的 javascript 事件

java - J2ME 数据库或数据管理

html - 在 th :href link 中使用模型属性

spring - @RequestMapping 注释中 "params"的含义?

web-services - 使用 Rest Web 服务的 Soap 客户端

java - 我可以拥有一组包含相同元素的集合吗?

java - Spring Boot 应用程序组织的最佳实践是什么?

java - 什么是用于覆盖参数的 AspectJ 声明语法