我正在将 Spring Security 与无状态 Web 服务结合使用。我想使用 Spring Security 3.2 中的 CSRF 功能。无状态网络应用程序可以实现这一点吗?
这是相关的Java配置,因为我不得不暂时禁用CSRF。
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.sessionFixation().none().and()
.csrf().disable();
}
如果你的服务确实是无状态的,CSRF 保护可能根本没有意义。只要服务器不使用 cookie 来识别/验证用户,您的服务就不易受到 CSRF 攻击。
详细说明请参见http://sitr.us/2011/08/26/cookies-are-bad-for-you.html
如果您的应用程序或API要通过浏览器访问,则需要CSRF保护。
参考演示:spring-security-and-angular-js
就像其他人所说,只有当您的应用程序将令牌存储在 cookie 中时才需要这样做。我最近不得不使用 Spring Security 6 来实现这一点,所以这就是我所采用的(用于自己进行身份验证的应用程序的解决方案)。
我发现使用自定义过滤器从 Spring Security 完全禁用 CSRF 广告检查(如本文章中所做的那样)的问题是,它还会关闭撤销 onAuthentication cookie,这可能允许会话固定攻击 - 攻击者在其中
由于这个原因,CSRF 代币应始终在本金更改时更改。
但是对于无状态应用程序,onAuthentication 会在每个请求上触发 - 这也不起作用,因为这将使前端在每个其他请求中都没有 CSRF 令牌。
根据 Spring Security Docu for SPA,我实现了
CsrfCookieFilter
和 CsrfTokenRequestAttributeHandler
,然后将其添加到 HttpSecurity
:
public class CsrfCookieFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
CsrfToken csrfToken = (CsrfToken) request.getAttribute("_csrf");
// Render the token value to a cookie by causing the deferred token to be loaded
csrfToken.getToken();
filterChain.doFilter(request, response);
}
}
public class SpaCsrfTokenRequestHandler extends CsrfTokenRequestAttributeHandler {
private final CsrfTokenRequestHandler delegate = new XorCsrfTokenRequestAttributeHandler();
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
Supplier<CsrfToken> csrfToken) {
/*
* Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of
* the CsrfToken when it is rendered in the response body.
*/
this.delegate.handle(request, response, csrfToken);
}
@Override
public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
/*
* If the request contains a request header, use CsrfTokenRequestAttributeHandler
* to resolve the CsrfToken. This applies when a single-page application includes
* the header value automatically, which was obtained via a cookie containing the
* raw CsrfToken.
*/
if (StringUtils.hasText(request.getHeader(csrfToken.getHeaderName()))) {
return super.resolveCsrfTokenValue(request, csrfToken);
}
/*
* In all other cases (e.g. if the request contains a request parameter), use
* XorCsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies
* when a server-side rendered form includes the _csrf request parameter as a
* hidden input.
*/
return this.delegate.resolveCsrfTokenValue(request, csrfToken);
}
}
然后我将这些添加到 HttpSecurity:
@Bean
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> {
CookieCsrfTokenRepository cookieCsrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
cookieCsrfTokenRepository.setCookiePath("/");
csrf.csrfTokenRepository(cookieCsrfTokenRepository)
.csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler())
.sessionAuthenticationStrategy(new NullAuthenticatedSessionStrategy()); // this turns OFF the onAuthenticate CSRF cookie revocation
})
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class)
.authorizeHttpRequests(customizer -> customizer.anyRequest().authenticated())
.build();
}
然后,我在初始 JWT 令牌发行(因此是第一次登录)后手动触发
onAuthentication
,以便在主体更改时撤销 CSRF 令牌。为此,我首先创建以下 bean:
@Bean
public CookieCsrfTokenRepository cookieCsrfTokenRepository() {
CookieCsrfTokenRepository cookieCsrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
cookieCsrfTokenRepository.setCookiePath("/");
return cookieCsrfTokenRepository;
}
@Bean
public CsrfAuthenticationStrategy csrfAuthenticationStrategy(CookieCsrfTokenRepository cookieCsrfTokenRepository) {
return new CsrfAuthenticationStrategy(cookieCsrfTokenRepository);
}
以及初次登录后拨打电话
onAuthentication
:
// called in the method that does the initial login (after Tokens are first created)
csrfAuthenticationStrategy.onAuthentication(authentication, request, response);
这会撤销 Csrf-Token Cookie,客户端在登录后必须再次发出 GET 请求(应该如此)。
@Injectable()
export class CsrfTokenInterceptor implements HttpInterceptor {
/**
* Methods allowed in Backend {@link CsrfFilter.DefaultRequiresCsrfMatcher#allowedMethods}
*/
private readonly CSRF_ALLOWED_METHODS = ["GET", "HEAD", "TRACE", "OPTIONS"]
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const xsrfTokenFromCookie = this.getCookie('XSRF-TOKEN');
// IF not a CSRF Allowed method (e.g. POST, PUT or DELETE), and XSRF-TOKEN is present
if (!this.CSRF_ALLOWED_METHODS.includes(req.method) && xsrfTokenFromCookie) {
// Clone the request and add X-XSRF-TOKEN header
const modifiedReq = req.clone({
headers: req.headers.set('X-XSRF-TOKEN', xsrfTokenFromCookie)
});
// Pass the modified request to the next handler
return next.handle(modifiedReq);
}
// If it's a GET request, pass the original request
return next.handle(req);
}
getCookie(name: string): string | undefined {
const cookies = document.cookie.split(';');
for (let cookie of cookies) {
const [cookieName, cookieValue] = cookie.trim().split('=');
if (cookieName === name) {
return cookieValue;
}
}
return undefined;
}
}
当然不要忘记将其添加到提供者中:
...
providers: [
{provide: HTTP_INTERCEPTORS, useClass: CsrfTokenInterceptor, multi:true}
],
...
此外,您还需要为将从后端获取 CSRF 令牌的请求添加选项
withCredentials: true
。
有一篇博客文章,更详细地解释了背景