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
服务,服务器响应与JSESSIONID
和XSRF-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-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开发人员工具的“网络”选项卡显示的内容:
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匹配,因此对其他后端休息服务的后续请求现在可以成功。
可以对下面的代码进行哪些具体更改,以便在第一次使用正确的用户名和密码调用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
中定义:
/user
服务返回的Principal。
但是需要做出哪些具体改变才能解决问题?
答案 0 :(得分:2)
获得401的原因是因为在用户注册时在请求中找到了基本身份验证标头。这意味着Spring Security尝试验证凭据,但用户尚未出现,因此它以401响应。
你应该
在进行身份验证后,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);
}