跨站请求伪造(CSRF)
本站(springdoc.cn)中的内容来源于 spring.io ,原始版权归属于 spring.io。由 springdoc.cn 进行翻译,整理。可供个人学习、研究,未经许可,不得进行任何转载、商用或与之相关的行为。 商标声明:Spring 是 Pivotal Software, Inc. 在美国以及其他国家的商标。 |
在终端用户可以 登录 的应用程序中,必须考虑如何防止 跨站请求伪造(CSRF)。
Spring Security 默认为 不安全的HTTP方法(如POST请求)提供CSRF攻击防护,因此无需额外代码。你可以使用下面的方法明确指定默认配置:
-
Java
-
Kotlin
-
XML
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.csrf(Customizer.withDefaults());
return http.build();
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
// ...
csrf { }
}
return http.build()
}
}
<http>
<!-- ... -->
<csrf/>
</http>
要了解有关应用程序 CSRF 保护的更多信息,请考虑以下用例:
-
我想 了解CSRF保护的组件。
-
我需要将一个应用程序 从 Spring Security 5 迁移到 Spring Security 6。
-
我想 将
CsrfToken
存储在 cookie 中,而不是 session 中。 -
我想 退出延迟的 token。
-
我想 退出 BREACH 保护。
-
我需要指导如何 将 Thymeleaf、JSP 或其他视图技术 与后台集成。
-
我需要将 Angular 或其他 JavaScript 框架 与后台集成的指导。
-
我需要将 移动应用程序或其他客户端 与后台集成的指导。
-
我需要有关 错误处理 的指导。
-
我想 测试 CSRF 保护
-
我需要关于 禁用 CSRF 保护 的指导。
了解 CSRF 保护的组件
CSRF 保护由 CsrfFilter
中的几个组件提供:
CsrfFilter
ComponentsCSRF 保护分为两部分:
-
通过委托
CsrfTokenRequestHandler
,使应用程序可以使用CsrfToken
。 -
确定请求是否需要 CSRF 保护,加载并验证 token,处理 handle
AccessDeniedException
。
CsrfFilter
Processing-
首先,
DeferredCsrfToken
被加载,其中保存了对CsrfTokenRepository
的引用,以便在稍后加载持久化的CsrfToken
(在第 步)。 -
其次,
Supplier<CsrfToken>
(由DeferredCsrfToken
创建)被提供给CsrfTokenRequestHandler
,CsrfTokenRequestHandler
负责填充请求属性,使CsrfToken
对应用程序的其他部分可用。 -
接下来,开始主要的 CSRF 保护处理,并检查当前请求是否需要CSRF保护。如果不需要,则继续 filter chain 并结束处理。
-
如果需要CSRF保护,则从
DeferredCsrfToken
中加载持久化的CsrfToken
。 -
继续使用
CsrfTokenRequestHandler
解析客户端提供的实际CSRF token(如有)。 -
将实际 CSRF token 与持久化的
CsrfToken
进行比较。如果有效,则继续 filter chain 并结束处理。 -
如果实际的 CSRF token 无效(或缺失),
AccessDeniedException
将被传递给AccessDeniedHandler
并结束处理。
迁移到 Spring Security 6
当从 Spring Security 5 迁移到 Spring Security 6 时,有一些变化可能会影响你的应用程序。以下是Spring Security 6 中 CSRF 保护方面变化的概述:
-
CsrfToken
的加载现在 默认为延迟加载,不再要求每次请求都加载 session,从而提高了性能。 -
The
CsrfToken
now includes randomness on every request by default to protect the CSRF token from a BREACH attack.
CsrfToken
现在默认在 每个请求中包含随机性,以保护 CSRF token 免受 BREACH 攻击。
Spring Security 6 中的变化要求对单页应用程序进行额外的配置,因此你可能会发现 单页应用(SPA) 部分特别有用。 |
持久化 CsrfToken
CsrfToken
使用 CsrfTokenRepository
持久化。
默认情况下,HttpSessionCsrfTokenRepository
用于在 session 中存储 token。Spring Security 还提供了用于在 cookie 中存储 token 的 CookieCsrfTokenRepository
。你也可以 指定自己的实现 来存储 token。
使用 HttpSessionCsrfTokenRepository
默认情况下,Spring Security 通过使用 HttpSessionCsrfTokenRepository
将预期的 CSRF 令牌存储在 HttpSession
中,因此无需额外代码。
HttpSessionCsrfTokenRepository
从名为 X-CSRF-TOKEN
的 HTTP 请求 header 或默认的请求参数 _csrf
中读取令牌。
你可以使用以下配置明确指定默认配置:
HttpSessionCsrfTokenRepository
-
Java
-
Kotlin
-
XML
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.csrf((csrf) -> csrf
.csrfTokenRepository(new HttpSessionCsrfTokenRepository())
);
return http.build();
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
// ...
csrf {
csrfTokenRepository = HttpSessionCsrfTokenRepository()
}
}
return http.build()
}
}
<http>
<!-- ... -->
<csrf token-repository-ref="tokenRepository"/>
</http>
<b:bean id="tokenRepository"
class="org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository"/>
使用 CookieCsrfTokenRepository
你可以使用 CookieCsrfTokenRepository
将 CsrfToken
持久化在 cookie 中,以支持 基于 JavaScript 的应用程序。
CookieCsrfTokenRepository
写入名为 XSRF-TOKEN
的 cookie,并从名为 X-XSRF-TOKEN
的 HTTP 请求头或默认的请求参数 _csrf
中读取。这些默认值来自 Angular 及其前身 AngularJS。
请参阅 跨站请求伪造(XSRF)保护 指南和 HttpClientXsrfModule,了解有关此主题的更多最新信息。 |
你可以使用以下配置配置 CookieCsrfTokenRepository
:
该示例明确地将 |
自定义 CsrfTokenRepository
在某些情况下,你需要实现一个自定义的 CsrfTokenRepository
。
一旦实现了 CsrfTokenRepository
接口,就可以通过以下配置来使用 Spring Security:
CsrfTokenRepository
-
Java
-
Kotlin
-
XML
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.csrf((csrf) -> csrf
.csrfTokenRepository(new CustomCsrfTokenRepository())
);
return http.build();
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
// ...
csrf {
csrfTokenRepository = CustomCsrfTokenRepository()
}
}
return http.build()
}
}
<http>
<!-- ... -->
<csrf token-repository-ref="tokenRepository"/>
</http>
<b:bean id="tokenRepository"
class="example.CustomCsrfTokenRepository"/>
处理 the CsrfToken
CsrfToken
通过 CsrfTokenRequestHandler
提供给应用程序。该组件还负责从 HTTP header 或请求参数中解析 CsrfToken
。
默认情况下,XorCsrfTokenRequestAttributeHandler
用于提供 CsrfToken
的 BREACH 保护。Spring Security 还提供了 CsrfTokenRequestAttributeHandler
用于选择退出 BREACH 保护。你也可以指定 自己的实现 来定制处理和解析 token 的策略。
使用 XorCsrfTokenRequestAttributeHandler
(BREACH)
XorCsrfTokenRequestAttributeHandler
使 CsrfToken
作为名为 _csrf
的 HttpServletRequest
attribute 可用,并提供额外的 BREACH 保护。
|
该实现还将来自请求的 token 值解析为请求头(默认为 X-CSRF-TOKEN
或 X-XSRF-TOKEN
之一)或请求参数(默认为 _csrf
)。
通过将随机性编码到 CSRF token 值中,以确保每次请求时返回的 |
Spring Security 默认保护 CSRF token 免受 BREACH 攻击,因此无需额外代码。你可以使用以下配置明确指定默认配置:
-
Java
-
Kotlin
-
XML
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.csrf((csrf) -> csrf
.csrfTokenRequestHandler(new XorCsrfTokenRequestAttributeHandler())
);
return http.build();
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
// ...
csrf {
csrfTokenRequestHandler = XorCsrfTokenRequestAttributeHandler()
}
}
return http.build()
}
}
<http>
<!-- ... -->
<csrf request-handler-ref="requestHandler"/>
</http>
<b:bean id="requestHandler"
class="org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler"/>
使用 CsrfTokenRequestAttributeHandler
CsrfTokenRequestAttributeHandler
使 CsrfToken
作为名为 _csrf
的 HttpServletRequest
attribute 可用。
|
该实现还将来自请求的 token 值解析为请求头(默认为 X-CSRF-TOKEN
或 X-XSRF-TOKEN
之一)或请求参数(默认为 _csrf
)。
CsrfTokenRequestAttributeHandler
的主要用途是选择退出 CsrfToken
的 BREACH 保护,可通过以下配置进行配置:
-
Java
-
Kotlin
-
XML
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.csrf((csrf) -> csrf
.csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())
);
return http.build();
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
// ...
csrf {
csrfTokenRequestHandler = CsrfTokenRequestAttributeHandler()
}
}
return http.build()
}
}
<http>
<!-- ... -->
<csrf request-handler-ref="requestHandler"/>
</http>
<b:bean id="requestHandler"
class="org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler"/>
自定义 CsrfTokenRequestHandler
你可以实现 CsrfTokenRequestHandler
接口来定制处理和解析 token 的策略。
|
一旦你实现了 CsrfTokenRequestHandler
接口,你就可以通过以下配置来使用 Spring Security:
CsrfTokenRequestHandler
-
Java
-
Kotlin
-
XML
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.csrf((csrf) -> csrf
.csrfTokenRequestHandler(new CustomCsrfTokenRequestHandler())
);
return http.build();
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
// ...
csrf {
csrfTokenRequestHandler = CustomCsrfTokenRequestHandler()
}
}
return http.build()
}
}
<http>
<!-- ... -->
<csrf request-handler-ref="requestHandler"/>
</http>
<b:bean id="requestHandler"
class="example.CustomCsrfTokenRequestHandler"/>
延迟加载 CsrfToken
默认情况下,Spring Security 会推迟加载 CsrfToken
,直到需要时才加载。
在使用 不安全 HTTP 方法(如 POST)进行请求时,都需要使用 |
由于 Spring Security 默认将 CsrfToken
存储在 HttpSession
中,因此延迟 CSRF token 无需在每个请求中加载 session,从而提高了性能。
如果你不希望使用延迟 token,而希望在每次请求时加载 CsrfToken
,可以通过以下配置实现:
-
Java
-
Kotlin
-
XML
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
XorCsrfTokenRequestAttributeHandler requestHandler = new XorCsrfTokenRequestAttributeHandler();
// set the name of the attribute the CsrfToken will be populated on
requestHandler.setCsrfRequestAttributeName(null);
http
// ...
.csrf((csrf) -> csrf
.csrfTokenRequestHandler(requestHandler)
);
return http.build();
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
val requestHandler = XorCsrfTokenRequestAttributeHandler()
// set the name of the attribute the CsrfToken will be populated on
requestHandler.setCsrfRequestAttributeName(null)
http {
// ...
csrf {
csrfTokenRequestHandler = requestHandler
}
}
return http.build()
}
}
<http>
<!-- ... -->
<csrf request-handler-ref="requestHandler"/>
</http>
<b:bean id="requestHandler"
class="org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler">
<b:property name="csrfRequestAttributeName">
<b:null/>
</b:property>
</b:bean>
通过将 |
与 CSRF 保护整合
为了使 同步 token 模式 能够抵御 CSRF 攻击,我们必须在HTTP请求中包含实际的 CSRF 令牌。这必须包含在请求的某个部分(表单参数、HTTP头或其他部分)中,浏览器不会自动将其包含在 HTTP 请求中。
以下章节介绍了前端或客户端应用程序与受 CSRF 保护的后端应用程序集成的各种方式:
HTML 表单
要提交 HTML 表单,CSRF 令牌必须作为 hidden input 包含在表单中。例如,渲染的 HTML 可能如下所示:
<input type="hidden"
name="_csrf"
value="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
以下视图技术会自动在具有不安全 HTTP 方法(如 POST)的表单中包含实际的 CSRF token:
-
与
RequestDataValueProcessor
集成的任何其他视图技术(通过CsrfRequestDataValueProcessor
)。 -
你也可以通过 csrfInput 标签自行包含 token。
如果没有这些选项,你可以利用 CsrfToken
作为 名为 _csrf
的 HttpServletRequest
属性 这一事实。下面的示例通过JSP实现了这一点:
<c:url var="logoutUrl" value="/logout"/>
<form action="${logoutUrl}"
method="post">
<input type="submit"
value="Log out" />
<input type="hidden"
name="${_csrf.parameterName}"
value="${_csrf.token}"/>
</form>
JavaScript 应用
JavaScript 应用程序通常使用 JSON 而不是 HTML。如果使用 JSON,则可以在 HTTP 请求头而不是请求参数中提交 CSRF token。
为了获取CSRF token,你可以配置 Spring Security 将预期的 CSRF token 存储在cookie中。通过在 cookie 中存储预期 token, Angular 等 JavaScript 框架可以自动将实际的 CSRF token 作为 HTTP 请求头。
在将单页面应用程序(SPA)与 Spring Security 的 CSRF 保护集成时,需要特别考虑 BREACH 保护和延迟 token。下一节 将提供完整的配置示例。 |
你可以在下面的章节中了解不同类型的 JavaScript 应用程序:
单页应用(SPA)
将单页应用程序(SPA)与 Spring Security 的 CSRF 保护集成在一起有一些特殊的注意事项。
回想一下,Spring Security 默认为 CsrfToken
提供 BREACH 保护。当 在 cookie 中 存储预期的 CSRF token 时,JavaScript 应用程序将只能访问纯 token 值,而无法访问编码值。需要提供 自定义的 request handler 来解析实际 token 值。
此外,存储 CSRF token 的 cookie 将在验证成功和注销成功时被清除。默认情况下,Spring Security会延迟加载新的 CSRF token,返回一个新的 cookie 需要额外的工作。
认证成功和注销成功后需要刷新令牌,因为 |
为了轻松地将单页应用程序与 Spring Security 集成,可以使用以下配置:
-
Java
-
Kotlin
-
XML
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.csrf((csrf) -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) (1)
.csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler()) (2)
)
.addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class); (3)
return http.build();
}
}
final 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);
}
}
final 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);
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
// ...
csrf {
csrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse() (1)
csrfTokenRequestHandler = SpaCsrfTokenRequestHandler() (2)
}
}
http.addFilterAfter(CsrfCookieFilter(), BasicAuthenticationFilter::class.java) (3)
return http.build()
}
}
class SpaCsrfTokenRequestHandler : CsrfTokenRequestAttributeHandler() {
private val delegate: CsrfTokenRequestHandler = XorCsrfTokenRequestAttributeHandler()
override fun handle(request: HttpServletRequest, response: HttpServletResponse, csrfToken: Supplier<CsrfToken>) {
/*
* Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of
* the CsrfToken when it is rendered in the response body.
*/
delegate.handle(request, response, csrfToken)
}
override fun resolveCsrfTokenValue(request: HttpServletRequest, csrfToken: CsrfToken): String {
/*
* 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.
*/
return if (StringUtils.hasText(request.getHeader(csrfToken.headerName))) {
super.resolveCsrfTokenValue(request, csrfToken)
} else {
/*
* 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.
*/
delegate.resolveCsrfTokenValue(request, csrfToken)
}
}
}
class CsrfCookieFilter : OncePerRequestFilter() {
@Throws(ServletException::class, IOException::class)
override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) {
val csrfToken = request.getAttribute("_csrf") as CsrfToken
// Render the token value to a cookie by causing the deferred token to be loaded
csrfToken.token
filterChain.doFilter(request, response)
}
}
<http>
<!-- ... -->
<csrf
token-repository-ref="tokenRepository" (1)
request-handler-ref="requestHandler"/> (2)
<custom-filter ref="csrfCookieFilter" after="BASIC_AUTH_FILTER"/> (3)
</http>
<b:bean id="tokenRepository"
class="org.springframework.security.web.csrf.CookieCsrfTokenRepository"
p:cookieHttpOnly="false"/>
<b:bean id="requestHandler"
class="example.SpaCsrfTokenRequestHandler"/>
<b:bean id="csrfCookieFilter"
class="example.CsrfCookieFilter"/>
1 | 配置 CookieCsrfTokenRepository 时,将 HttpOnly 设置为 false ,这样 JavaScript 应用程序就可以读取 Cookie。 |
2 | 配置自定义的 CsrfTokenRequestHandler ,根据 CSRF token 是 HTTP 请求头(X-XSRF-TOKEN )还是请求参数(_csrf )来解析 CSRF token。 |
3 | 配置一个自定义 Filter ,在每次请求时加载 CsrfToken ,如果需要,将返回一个新的 cookie。 |
多页应用
对于在每个页面上加载 JavaScript 的多页面应用程序,除了 在 cookie 中 公开 CSRF token 外,还可以在 meta
标签中包含 CSRF token。HTML可能如下所示
<html>
<head>
<meta name="_csrf" content="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
<meta name="_csrf_header" content="X-CSRF-TOKEN"/>
<!-- ... -->
</head>
<!-- ... -->
</html>
为了在请求中包含 CSRF token,可以利用 CsrfToken
作为 名为 _csrf
的 HttpServletRequest
属性 的事实。下面的示例通过JSP实现了这一点:
<html>
<head>
<meta name="_csrf" content="${_csrf.token}"/>
<!-- default header name is X-CSRF-TOKEN -->
<meta name="_csrf_header" content="${_csrf.headerName}"/>
<!-- ... -->
</head>
<!-- ... -->
</html>
一旦 meta 标签包含 CSRF token,JavaScript 代码就可以读取 meta 标签,并将 CSRF token 作为 header 包含在内。如果你使用 jQuery,你可以通过以下代码来实现:
$(function () {
var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");
$(document).ajaxSend(function(e, xhr, options) {
xhr.setRequestHeader(header, token);
});
});
其他 JavaScript 应用
JavaScript 应用程序的另一种选择是在 HTTP 响应头中包含 CSRF 令牌。
实现这一点的方法之一是通过使用 CsrfTokenArgumentResolver
的 @ControllerAdvice
。下面是一个 @ControllerAdvice
的示例,它适用于应用程序中的所有 controller 端点:
-
Java
-
Kotlin
@ControllerAdvice
public class CsrfControllerAdvice {
@ModelAttribute
public void getCsrfToken(HttpServletResponse response, CsrfToken csrfToken) {
response.setHeader(csrfToken.getHeaderName(), csrfToken.getToken());
}
}
@ControllerAdvice
class CsrfControllerAdvice {
@ModelAttribute
fun getCsrfToken(response: HttpServletResponse, csrfToken: CsrfToken) {
response.setHeader(csrfToken.headerName, csrfToken.token)
}
}
由于该 |
重要的是要记住,controller 端点和 controller advice 是在 Spring Security filter chain 之后调用的。这意味着只有当请求通过 filter chain 到达你的应用程序时,才会应用 |
对于 controller advice 适用的任何自定义端点,CSRF token 现在可以在响应头(默认为X-CSRF-TOKEN
或X-XSRF-TOKEN
)中获得。对后端的任何请求都可用于从响应中获取 token,后续请求可将 token 包含在具有相同名称的请求头中。
移动端应用
与 JavaScript 应用程序 一样,移动应用程序通常使用 JSON 而不是HTML。不提供浏览器流量的后端应用程序可以选择 禁用 CSRF。在这种情况下,无需额外工作。
但是,如果后端应用程序也提供浏览器流量,因此仍然需要 CSRF 保护,则可以继续将 CsrfToken
存储在 session 中,而不是 cookie 中。
在这种情况下,与后端集成的典型模式是暴露 /csrf
端点,允许前端(移动或浏览器客户端)按需请求 CSRF token。使用这种模式的好处是,CSRF token 可以继续延迟,只有在请求需要 CSRF 保护时才需要从会话中加载。使用自定义端点还意味着客户端应用程序可以通过发出显式请求,要求按需生成新 token(如有必要)。
这种模式可用于需要 CSRF 保护的任何类型的应用程序,而不仅仅是移动应用程序。虽然在这些情况下通常不需要这种方法,但它是与受 CSRF 保护的后端集成的另一种选择。 |
下面是一个使用 CsrfTokenArgumentResolver
的 /csrf
端点示例:
/csrf
endpoint-
Java
-
Kotlin
@RestController
public class CsrfController {
@GetMapping("/csrf")
public CsrfToken csrf(CsrfToken csrfToken) {
return csrfToken;
}
}
@RestController
class CsrfController {
@GetMapping("/csrf")
fun csrf(csrfToken: CsrfToken): CsrfToken {
return csrfToken
}
}
如果上述端点需要与服务器进行身份验证,你可以考虑添加 |
在应用程序启动或初始化时(如加载时),以及在身份验证成功和注销成功后,应调用该端点以获取 CSRF token。
认证成功和注销成功后需要刷新 token,因为 |
获得CSRF令牌后,你需要将其作为 HTTP 请求头(默认为 X-CSRF-TOKEN
或 X-XSRF-TOKEN
之一)。
处理 AccessDeniedException
要处理诸如 InvalidCsrfTokenException
之类的 AccessDeniedException
,可以配置 Spring Security 以任何方式处理这些异常。例如,你可以使用以下配置配置自定义拒绝访问页面:
AccessDeniedHandler
-
Java
-
Kotlin
-
XML
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.exceptionHandling((exceptionHandling) -> exceptionHandling
.accessDeniedPage("/access-denied")
);
return http.build();
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
// ...
exceptionHandling {
accessDeniedPage = "/access-denied"
}
}
return http.build()
}
}
<http>
<!-- ... -->
<access-denied-handler error-page="/access-denied"/>
</http>
CSRF 测试
你可以使用 Spring Security 的 测试支持 和 CsrfRequestPostProcessor
来测试 CSRF 保护,就像这样:
-
Java
-
Kotlin
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = SecurityConfig.class)
@WebAppConfiguration
public class CsrfTests {
private MockMvc mockMvc;
@BeforeEach
public void setUp(WebApplicationContext applicationContext) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(applicationContext)
.apply(springSecurity())
.build();
}
@Test
public void loginWhenValidCsrfTokenThenSuccess() throws Exception {
this.mockMvc.perform(post("/login").with(csrf())
.accept(MediaType.TEXT_HTML)
.param("username", "user")
.param("password", "password"))
.andExpect(status().is3xxRedirection())
.andExpect(header().string(HttpHeaders.LOCATION, "/"));
}
@Test
@WithMockUser
public void logoutWhenValidCsrfTokenThenSuccess() throws Exception {
this.mockMvc.perform(post("/logout").with(csrf())
.accept(MediaType.TEXT_HTML))
.andExpect(status().is3xxRedirection())
.andExpect(header().string(HttpHeaders.LOCATION, "/login?logout"));
}
}
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.*
@ExtendWith(SpringExtension::class)
@ContextConfiguration(classes = [SecurityConfig::class])
@WebAppConfiguration
class CsrfTests {
private lateinit var mockMvc: MockMvc
@BeforeEach
fun setUp(applicationContext: WebApplicationContext) {
mockMvc = MockMvcBuilders.webAppContextSetup(applicationContext)
.apply<DefaultMockMvcBuilder>(springSecurity())
.build()
}
@Test
fun loginWhenValidCsrfTokenThenSuccess() {
mockMvc.perform(post("/login").with(csrf())
.accept(MediaType.TEXT_HTML)
.param("username", "user")
.param("password", "password"))
.andExpect(status().is3xxRedirection)
.andExpect(header().string(HttpHeaders.LOCATION, "/"))
}
@Test
@WithMockUser
@Throws(Exception::class)
fun logoutWhenValidCsrfTokenThenSuccess() {
mockMvc.perform(post("/logout").with(csrf())
.accept(MediaType.TEXT_HTML))
.andExpect(status().is3xxRedirection)
.andExpect(header().string(HttpHeaders.LOCATION, "/login?logout"))
}
}
禁用 CSRF 保护
默认情况下,CSRF 保护是启用的,这会影响 与后台的集成 和应用程序的 测试。在禁用 CSRF 保护之前,请考虑这 对你的应用程序是否有意义。
你还可以考虑是否只有某些端点不需要 CSRF 保护,并配置忽略规则,如下例所示:
-
Java
-
Kotlin
-
XML
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.csrf((csrf) -> csrf
.ignoringRequestMatchers("/api/*")
);
return http.build();
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
// ...
csrf {
ignoringRequestMatchers("/api/*")
}
}
return http.build()
}
}
<http>
<!-- ... -->
<csrf request-matcher-ref="csrfMatcher"/>
</http>
<b:bean id="csrfMatcher"
class="org.springframework.security.web.util.matcher.AndRequestMatcher">
<b:constructor-arg value="#{T(org.springframework.security.web.csrf.CsrfFilter).DEFAULT_CSRF_MATCHER}"/>
<b:constructor-arg>
<b:bean class="org.springframework.security.web.util.matcher.NegatedRequestMatcher">
<b:bean class="org.springframework.security.web.util.matcher.AntPathRequestMatcher">
<b:constructor-arg value="/api/*"/>
</b:bean>
</b:bean>
</b:constructor-arg>
</b:bean>
如果需要禁用 CSRF 保护,可以使用以下配置:
-
Java
-
Kotlin
-
XML
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.csrf((csrf) -> csrf.disable());
return http.build();
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
// ...
csrf {
disable()
}
}
return http.build()
}
}
<http>
<!-- ... -->
<csrf disabled="true"/>
</http>
CSRF 注意事项
在实施 CSRF 攻击防护时,有一些特殊的注意事项。本节将讨论与 servlet 环境相关的注意事项。有关更一般性的讨论,请参阅 CSRF 注意事项。
登录
对 登录请求要求 CSRF 以防止伪造登录尝试是很重要的。Spring Security 的 Servlet 支持就能做到这一点。
注销
对 注销请求要求 CSRF 以防止伪造注销尝试是很重要的。如果启用了CSRF保护(默认),Spring Security 的 LogoutFilter
将只处理 HTTP POST 请求。这将确保注销需要 CSRF token,恶意用户无法强行注销用户。
最简单的方法是使用表单注销用户。如果你真的需要一个链接,你可以使用 JavaScript 让链接执行 POST(也许在一个 hidden 表单上)。对于禁用 JavaScript 的浏览器,你可以选择让链接将用户带到一个执行 POST 的注销确认页面。
如果你真的想在注销时使用 HTTP GET,你可以这样做。但请记住,一般不建议这样做。例如,当使用任何 HTTP 方法请求 /logout
URL时,将执行以下操作进行注销:
-
Java
-
Kotlin
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.logout((logout) -> logout
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
);
return http.build();
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
// ...
logout {
logoutRequestMatcher = AntPathRequestMatcher("/logout")
}
}
return http.build()
}
}
更多信息请参阅 注销 章节。
CSRF 和 session 超时
默认情况下,Spring Security 使用 HttpSessionCsrfTokenRepository
将 CSRF token 存储在 HttpSession
中。这可能会导致 session 过期,从而没有 CSRF token 进行验证。
我们已经讨论了 session 超时的 一般解决方案。本节将讨论与 servlet 支持相关的 CSRF 超时的具体问题。
你可以将 CSRF token 存储在 cookie 中。详情请参阅 使用 CookieCsrfTokenRepository
部分。
如果 token 过期,你可能希望通过指定 自定义的 AccessDeniedHandler
来自定义处理方式。自定义的 AccessDeniedHandler
可以任意处理 InvalidCsrfTokenException
。
Multipart(文件上传)
我们 已经讨论过 保护 multipart 请求(文件上传)免受CSRF攻击如何导致 鸡和蛋 的问题。当 JavaScript 可用时,我们建议 将 CSRF token 包含在 HTTP 请求头中,以避免这一问题。
你可以在 Spring 参考文档的 Multipart Resolver 部分和 |
把 CSRF Token 放到 body 中
我们 已经讨论了 将 CSRF token 放在 body 中的利弊。在本节中,我们将讨论如何配置 Spring Security 以从 body 中读取 CSRF。
为了从 body 中读取 CSRF token,在 Spring Security filter 之前指定了 MultipartFilter
。在 Spring Security filter 之前指定 MultipartFilter
意味着调用 MultipartFilter
没有授权,这意味着任何人都可以在服务器上放置临时文件。然而,只有经过授权的用户才能提交由应用程序处理的文件。一般来说,这是推荐的方法,因为临时文件上传对大多数服务器的影响可以忽略不计。
MultipartFilter
-
Java
-
Kotlin
-
XML
public class SecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
@Override
protected void beforeSpringSecurityFilterChain(ServletContext servletContext) {
insertFilters(servletContext, new MultipartFilter());
}
}
class SecurityApplicationInitializer : AbstractSecurityWebApplicationInitializer() {
override fun beforeSpringSecurityFilterChain(servletContext: ServletContext?) {
insertFilters(servletContext, MultipartFilter())
}
}
<filter>
<filter-name>MultipartFilter</filter-name>
<filter-class>org.springframework.web.multipart.support.MultipartFilter</filter-class>
</filter>
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>MultipartFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
为了确保 |
在 ULR 中包含 CSRF Token
如果不能接受让未经授权的用户上传临时文件,另一种方法是将 MultipartFilter
放在 Spring Security filter 之后,并将 CSRF 作为查询参数包含在表单的 action
属性中。由于 CsrfToken
是作为 名为 _csrf
的 HttpServletRequest
属性 公开的,因此我们可以使用它来创建一个包含 CSRF token 的 action
。下面的示例通过 JSP 实现了这一点:
<form method="post"
action="./upload?${_csrf.parameterName}=${_csrf.token}"
enctype="multipart/form-data">
HiddenHttpMethodFilter
我们 已经讨论过 在 body 中放置 CSRF token 的利弊。
在 Spring 的 Servlet 支持中,覆盖 HTTP 方法是通过使用 HiddenHttpMethodFilter
来实现的。你可以在参考文档的 HTTP 方法转换 部分找到更多信息。