当用户进行身份验证时,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令牌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/