如何强制 Spring Security 更新 XSRF-TOKEN cookie? [英] How do I force Spring Security to update XSRF-TOKEN cookie?

查看:56
本文介绍了如何强制 Spring Security 更新 XSRF-TOKEN cookie?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

A REST Spring Security /user service in a Spring Boot application is failing to immediately update the XSRF-TOKEN cookie when a user authenticates. This is causing the next request for /any-other-REST-service-url to return an Invalid CSRF certificate error, until the /user service is called again. How can this problem be resolved so that the REST /user service properly updates the XSRF-TOKEN cookie in the same request/response transaction in whichit first authenticates the user?

后端 REST /user 服务被前端应用调用了 3 次,但 /user 服务只返回匹配的 JSESSIONID/XSRF-TOKEN cookie 在第一次和第三次调用中,而不是在第二次调用中.

The backend REST /user service is called three times by a front end app, but the /user service only returns matched JSESSIONID/XSRF-TOKEN cookies on the first and third call, NOT on the second call.

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

  1. In the first request to the server, no credentials (no username or password) are sent to the / url pattern, which I think calls the /user service, and the server responds with a JSESSIONID and XSRF-TOKEN that it associated with an anonymous user. The Network tab of the FireFox developer tools shows these cookies as:

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

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

The user then makes various requests for publicly accessible resources without error, and the Network tab of the FireFox developer tools shows these same cookie values.

/user 服务的第二个请求是通过登录表单完成的,该表单发送一个有效的用户名和密码,/user 服务使用这些用户名和密码验证用户.但是/user服务只返回一个更新后的jsessionid cookie,并没有在这一步更新xsrf-token cookie.以下是此时 FireFox 开发人员工具的网络"选项卡中显示的 cookie:

The second request to the /user service is done though a login form, which sends a valid username and password, which the/user service uses to authenticate the user. But the /user service only returns an updated jsessionid cookie, and does not update the xsrf-token cookie in this step. Here are the cookies shown in the Network tab of the FireFox developer tools at this point:

200 GET user 在 FireFox 的网络"选项卡中包含以下 cookie:

The 200 GET user included the following cookies in the Network tab of FireFox:

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 ?

Note that the response included a new JSESSIONID, but did not include a new XSRF-TOKEN. This results in a mismatch causing a 403 error (due to invalid csrf token) in the subsequent requests to other rest services, until this is resolved by a third call to the /user service. is there a way that we can force the preceding 200 get user to return the new XSRF-TOKEN also?

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

The third call to the backend REST /user service uses the very same username and password credentials that were used in the second request shown above, but this third call to /user results in the XSRF_TOKEN cookie being updated properly, while the same correct JSESSIONID is retained. Here is what the Network tab of the FireFox developer tools shows at this point:

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

The 200 GET user shows that the mismatched request forces an update of the XSRF-TOKEN in the response:

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

The updated xsrf-token now matches the jsessionid, and thus subsequent requests to other backend rest services can now succeed.

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

What specific changes can be made to the code below to force an update of both the XSRF-TOKEN and the JSESSIONID cookies the first time the /user service is called with proper username and password by the login form? Do we make specific changes in the code for the backend /user method in Spring? Or is the change made in the Security Configuration classes? What can we try to fix this problem?

后端/user服务和Security Config的代码在Spring Boot后端应用的主应用类中,在UiApplication.java中如下:

The code for the backend /user service and the Security Config are in the main application class of the Spring Boot backend app, which is in UiApplication.java as follows:

@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错误的服务器日志的相关部分是:

The relevant segment of the server logs showing the CSRF error is:

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 错误?

What specific changes do I need to make to the code above to resolve this CSRF error?

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

How do I force an immediate update of the XSRF cookie upon whenever the backend /user service changes a user's status (login, logout, etc.)?

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

Note: I am guessing (based on my research) that the solution to this problem will involve changing the configuration of some combination of the following Spring Security classes, all of which are defined in the UiApplication.java shown below:

  1. WebSecurityConfigurerAdapter,

OncePerRequestFilter,

CsrfTokenRepository,

GlobalAuthenticationConfigurerAdapter 和/或

返回的 Principal通过 /user 服务.

the Principal returned by the /user service.

但是需要进行哪些具体的更改才能解决问题?

But what specific changes need to be made to solve the problem?

推荐答案

更新答案

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

Updated Answer

The reason you are getting a 401 is because a basic authentication header is found in the request when the user is registering. This means Spring Security tries to validate the credentials but the user is not yet present so it responds with a 401.

你应该

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

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

After authenticating, Spring Security uses CsrfAuthenticationStrategy to invalidate any CsrfToken's (to ensure that a session fixation attack is not possible). This is what triggers a new CsrfToken to be used.

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

However, the problem is that csrfTokenRepository is invoked before authentication is performed. This means that when csrfTokenRepository checks to see if the token has changed the result if false (it has not changed yet).

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

To resolve the issue, you can inject a custom AuthenticationSuccessHandler. For example:

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);
    }

这篇关于如何强制 Spring Security 更新 XSRF-TOKEN cookie?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆