我对Spring Security的理解是,认证的时候,在认证之前会生成一个csrf token,在认证成功之后再生成一个。
我注意到身份验证后在响应标头中返回了错误的 csrf 令牌,然后在尝试命中 POST 端点时导致 403 响应(无效的 CSRF 令牌)。该项目可以在github上找到;在这里。以下是一些特定于安全性的代码:
弹簧安全配置:
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity httpSecurity) throws Exception {
CsrfTokenRepository csrfTokenRepository = new HttpSessionCsrfTokenRepository();
httpSecurity
.authorizeRequests().anyRequest().authenticated().and()
//.antMatchers("/status").permitAll().and()
.cors().and().csrf().csrfTokenRepository(csrfTokenRepository).and()
.authenticationProvider(new BasicAuthenticationProvider())
.addFilterAfter(new CsrfTokenResponseHeaderBindingFilter(csrfTokenRepository), CsrfFilter.class)
.addFilterBefore(new CustomBasicAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(new ArrayList<>());
configuration.setAllowedMethods(ImmutableList.of("GET", "POST"));
configuration.setAllowedHeaders(ImmutableList.of("*"));
configuration.setExposedHeaders(ImmutableList.of("X-Csrf-Token"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
在响应头中添加 CSRF 令牌的过滤器:
public class CsrfTokenResponseHeaderBindingFilter extends OncePerRequestFilter {
private static final String CSRF_TOKEN_NAME = "x-csrf-token";
private CsrfTokenRepository csrfTokenRepository;
public CsrfTokenResponseHeaderBindingFilter(CsrfTokenRepository csrfTokenRepository) {
this.csrfTokenRepository = csrfTokenRepository;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
response.addHeader(CSRF_TOKEN_NAME, csrfTokenRepository.loadToken(request).getToken());
System.out.println("shoop: " + csrfTokenRepository.loadToken(request).getToken());
filterChain.doFilter(request, response);
System.out.println("shap: " + csrfTokenRepository.loadToken(request).getToken());
}
}
过滤基本身份验证:
public class CustomBasicAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
BasicAuthenticationToken basicAuthenticationToken = generateBasicAuthenticationToken(request);
if (basicAuthenticationToken != null) {
SecurityContextHolder.getContext().setAuthentication(basicAuthenticationToken);
}
filterChain.doFilter(request, response);
}
private BasicAuthenticationToken generateBasicAuthenticationToken(HttpServletRequest request) {
System.out.println("generateBasicAuthenticationToken");
String authorizationHeaderValue = request.getHeader("Authorization");
BasicAuthenticationToken basicAuthenticationToken = null;
if (authorizationHeaderValue != null && authorizationHeaderValue.startsWith("Basic ")) {
System.out.println("basic authentication header provided");
final String base64EncodedCredentials = authorizationHeaderValue.replace("Basic ", "").trim();
final byte[] decodedCredentials = Base64.getDecoder().decode(base64EncodedCredentials);
final String credentials = new String(decodedCredentials);
final String[] splitCredentials = credentials.split(":", 2);
System.out.println("credentials: " + credentials);
System.out.println("username: " + splitCredentials[0]);
System.out.println("password: " + splitCredentials[1]);
basicAuthenticationToken = new BasicAuthenticationToken (splitCredentials[0], splitCredentials[1]);
}
return basicAuthenticationToken;
}
}
基本身份验证提供程序:
public class BasicAuthenticationProvider implements AuthenticationProvider {
private final String VALID_USERNAME = "user";
private final String VALID_PASSWORD = "pass";
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
System.out.println("BasicAuthenticationProvider");
String username = authentication.getName();
String password = (String) authentication.getCredentials();
if (!username.equals(VALID_USERNAME) || !password.equals(VALID_PASSWORD)) {
throw new InvalidCredentialsException("Invalid credentials provided.");
}
return new UsernamePasswordAuthenticationToken(username, "", Collections.emptyList());
}
@Override
public boolean supports(Class<?> aClass) {
return BasicAuthenticationToken.class.equals(aClass);
}
}
为了确认问题与无效的 csrf 令牌有关,我打印了 spring 在身份验证前后生成的 csrf 令牌,并将这些令牌与响应标头中的令牌进行了比较。此外,当我使用调试级日志记录运行应用程序时,在我通过身份验证后尝试访问经过身份验证的 POST 端点时,我会从 spring 收到以下错误消息:Invalid CSRF token found for http://localhost:8080/auth/status
执行此测试时,我使用了以下 curl 命令:
curl -XGET 'http://localhost:8080/login' -H 'Authorization: Basic dXNlcjpwYXNz' -v
curl -XPOST 'http://localhost:8080/auth/status' -H 'Cookie: <session id>' -H 'x-csrf-token: <csrf token>' -v
PS:对于在本地运行我上面链接的项目的任何人,当您运行脚本/构建时,您将被要求在本地计算机上提供您的凭据。这是因为我在 linux 机器上使用 docker,这需要超级用户权限。