|
对于最新的稳定版本,请使用 Spring Security 7.0.4! |
跨站请求伪造(CSRF)
在一个允许最终用户登录的应用程序中,考虑如何防范跨站请求伪造(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 防护信息,请考虑以下使用场景:
-
我想将
CsrfToken存储在 cookie 中,而不是 会话 中 -
我希望将
CsrfToken存储在自定义位置 -
我需要指导,将Thymeleaf、JSP 或其他视图技术与后端集成
-
我需要指导,将Angular 或其他 JavaScript 框架与后端集成
-
我需要指导,将移动应用程序或其他客户端与后端集成
-
我需要关于处理错误的指导
-
我需要关于禁用 CSRF 保护的指导
了解 CSRF 保护的组件
CSRF 保护由多个组件提供,这些组件组合在 CsrfFilter 中:
CsrfFilter 组件CSRF 保护分为两个部分:
-
通过委托给
CsrfTokenRequestHandler,使CsrfToken对应用程序可用。 -
确定请求是否需要 CSRF 保护,加载并验证Tokens,然后 处理
AccessDeniedException。
CsrfFilter 处理流程-
首先,DeferredCsrfToken已加载,其中包含对CsrfTokenRepository以便持久化CsrfToken可以在以后加载(在
). -
其次,Supplier<CsrfToken>(由DeferredCsrfToken) 被赋予给CsrfTokenRequestHandler,负责填充请求属性以CsrfToken可供应用程序其余部分使用。 -
接下来,主要的 CSRF 保护处理开始,并检查当前请求是否需要 CSRF 保护。如果不需要,则继续过滤器链并结束处理。 -
如果需要 CSRF 保护,则持久化的CsrfToken终于从DeferredCsrfToken. -
继续,由客户端提供的实际 CSRF Tokens(如果有)将通过以下方式解析CsrfTokenRequestHandler. -
实际的 CSRF Tokens与持久化的Tokens进行比较CsrfToken。如果有效,则继续过滤器链并结束处理。 -
如果实际的 CSRF Tokens无效(或缺失),AccessDeniedException被传递给AccessDeniedHandler处理结束。
持久化CsrfToken
CsrfToken 使用 CsrfTokenRepository 进行持久化。
默认情况下,HttpSessionCsrfTokenRepository 用于在会话中存储Tokens。
Spring Security 还提供了 CookieCsrfTokenRepository 用于在 Cookie 中存储Tokens。
您还可以指定 您自己的实现,以便将Tokens存储在您喜欢的任何位置。
使用HttpSessionCsrfTokenRepository
默认情况下,Spring Security 使用 HttpSessionCsrfTokenRepository 将预期的 CSRF Tokens存储在 HttpSession 中,因此无需额外代码。
HttpSessionCsrfTokenRepository 从会话(无论是内存、缓存还是数据库中的会话)中读取Tokens。如果您需要直接访问会话属性,请首先使用 HttpSessionCsrfTokenRepository#setSessionAttributeName 配置会话属性名称。
你可以使用以下配置显式指定默认配置:
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
您可以将 CsrfToken 持久化到 Cookie 中,以使用 CookieCsrfTokenRepository 支持基于 JavaScript 的应用程序 。
CookieCsrfTokenRepository 默认会将 CSRF Tokens写入名为 XSRF-TOKEN 的 Cookie 中,并从名为 X-XSRF-TOKEN 的 HTTP 请求头或名为 _csrf 的请求参数中读取该Tokens。
这些默认设置源自 Angular 及其前身 AngularJS。
|
有关此主题的最新信息,请参阅HttpClient XSRF/CSRF 安全性和withXsrfConfiguration。 |
您可以使用以下配置来设置 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"/>
处理CsrfToken
应用程序通过 CsrfToken 获取 CsrfTokenRequestHandler。
该组件还负责从 HTTP 头部或请求参数中解析 CsrfToken。
默认情况下,使用 XorCsrfTokenRequestAttributeHandler 来提供 BREACH 防护,以保护 CsrfToken。
Spring Security 还提供了 CsrfTokenRequestAttributeHandler,用于选择退出 BREACH 防护。
您也可以指定 自己的实现,以自定义处理和解析Tokens的策略。
使用XorCsrfTokenRequestAttributeHandler(入侵)
XorCsrfTokenRequestAttributeHandler 将 CsrfToken 作为名为 HttpServletRequest 的 _csrf 属性提供,并额外提供了针对 BREACH 攻击的防护。
|
|
此实现还会从请求中解析Tokens值,作为请求头(默认情况下为 X-CSRF-TOKEN 或 X-XSRF-TOKEN)或请求参数(默认为 _csrf)。
|
BREACH 防护通过随机性编码到 CSRF Tokens值中来实现,以确保每次请求返回的 |
Spring Security 默认会保护 CSRF Tokens免受 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 作为名为 HttpServletRequest 的 _csrf 属性提供。
|
|
此实现还会从请求中解析Tokens值,作为请求头(默认情况下为 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 接口,以自定义处理和解析Tokens的策略。
|
|
一旦你实现了 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 Tokens可以通过避免在每个请求中都加载会话来提升性能。
如果你希望退出延迟加载Tokens,并在每个请求中都加载 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 保护集成
为了使用同步Tokens模式来防范 CSRF 攻击,我们必须在 HTTP 请求中包含实际的 CSRF Tokens。 该Tokens必须包含在请求的某个部分(例如表单参数、HTTP 头部或其他部分)中,而这一部分不能由浏览器自动包含在 HTTP 请求中。
以下各节介绍了前端或客户端应用程序与启用了 CSRF 保护的后端应用程序进行集成的各种方式:
HTML 表单
要提交一个 HTML 表单,CSRF Tokens必须作为隐藏输入字段包含在表单中。 例如,渲染后的 HTML 可能如下所示:
<input type="hidden"
name="_csrf"
value="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
以下视图技术会自动在使用不安全 HTTP 方法(例如 POST)的表单中包含实际的 CSRF Tokens:
-
任何其他与
RequestDataValueProcessor集成的视图技术(通过CsrfRequestDataValueProcessor) -
您也可以通过 csrfInput 标签自行包含该Tokens
如果这些选项不可用,您可以利用这样一个事实: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,您可以将 CSRF Tokens放在 HTTP 请求头中提交,而不是作为请求参数。
为了获取 CSRF Tokens,您可以配置 Spring Security 将预期的 CSRF Tokens存储在 Cookie 中。 通过将预期的Tokens存储在 Cookie 中,诸如Angular之类的 JavaScript 框架可以自动将实际的 CSRF Tokens作为 HTTP 请求头包含进去。
|
在将单页应用(SPA)与 Spring Security 的 CSRF 保护集成时,对于 BREACH 防护和延迟Tokens(deferred tokens)有一些特殊注意事项。 完整的配置示例请参见下一节。 |
您可以在以下章节中了解不同类型的 JavaScript 应用程序:
单页应用程序
将单页应用(SPA)与 Spring Security 的 CSRF 保护机制集成时,有一些特殊注意事项。
请记住,Spring Security 默认提供对CsrfToken的 BREACH 防护。
当将预期的 CSRF Tokens存储为cookie时,JavaScript 应用程序仅能访问纯文本Tokens值,而无法访问编码后的值。
因此,需要提供一个用于解析实际Tokens值的自定义请求处理器。
此外,在认证成功和注销成功时,用于存储 CSRF Tokens的 Cookie 将被清除。 Spring Security 默认会延迟加载新的 CSRF Tokens,若要返回一个新的 Cookie,需要进行额外的处理。
|
在认证成功和登出成功后需要刷新Tokens,因为 |
为了轻松地将单页应用程序与 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)
);
return http.build();
}
}
final class SpaCsrfTokenRequestHandler implements CsrfTokenRequestHandler {
private final CsrfTokenRequestHandler plain = new CsrfTokenRequestAttributeHandler();
private final CsrfTokenRequestHandler xor = 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.xor.handle(request, response, csrfToken);
/*
* Render the token value to a cookie by causing the deferred token to be loaded.
*/
csrfToken.get();
}
@Override
public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
String headerValue = request.getHeader(csrfToken.getHeaderName());
/*
* 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.
*
* 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 (StringUtils.hasText(headerValue) ? this.plain : this.xor).resolveCsrfTokenValue(request, csrfToken);
}
}
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)
}
}
return http.build()
}
}
class SpaCsrfTokenRequestHandler : CsrfTokenRequestHandler {
private val plain: CsrfTokenRequestHandler = CsrfTokenRequestAttributeHandler()
private val xor: 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.
*/
xor.handle(request, response, csrfToken)
/*
* Render the token value to a cookie by causing the deferred token to be loaded.
*/
csrfToken.get()
}
override fun resolveCsrfTokenValue(request: HttpServletRequest, csrfToken: CsrfToken): String? {
val headerValue = request.getHeader(csrfToken.headerName)
/*
* 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(headerValue)) {
plain
} 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.
*/
xor
}.resolveCsrfTokenValue(request, csrfToken)
}
}
<http>
<!-- ... -->
<csrf
token-repository-ref="tokenRepository" (1)
request-handler-ref="requestHandler"/> (2)
</http>
<b:bean id="tokenRepository"
class="org.springframework.security.web.csrf.CookieCsrfTokenRepository"
p:cookieHttpOnly="false"/>
<b:bean id="requestHandler"
class="example.SpaCsrfTokenRequestHandler"/>
| 1 | 配置CookieCsrfTokenRepository,将HttpOnly设置为false,以便JavaScript应用能够读取该cookie。 |
| 2 | 配置一个自定义的CsrfTokenRequestHandler,该CsrfTokenRequestHandler根据是否是HTTP请求头(X-XSRF-TOKEN)或请求参数(_csrf)来解析CSRFTokens。
此实现还会导致延迟加载CsrfToken在每次请求中,如果需要的话会返回一个新的cookie。 |
多页面应用程序
对于在每个页面都加载 JavaScript 的多页应用程序,除了将 CSRF Tokens暴露在 Cookie 中之外,另一种方法是将 CSRF Tokens包含在您的 meta 标签中。
HTML 代码可能如下所示:
<html>
<head>
<meta name="_csrf" content="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
<meta name="_csrf_header" content="X-CSRF-TOKEN"/>
<!-- ... -->
</head>
<!-- ... -->
</html>
为了在请求中包含 CSRF Tokens,您可以利用 CsrfToken 作为 HttpServletRequest 属性名为 _csrf 公开这一事实。
以下示例展示了如何在 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 Tokens,JavaScript 代码就可以读取这些 meta 标签,并将 CSRF Tokens作为请求头包含进去。 如果你使用 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 应用程序的另一种选择是将 CSRF Tokens包含在 HTTP 响应头中。
一种实现方法是使用带有 CsrfTokenArgumentResolver 的 @ControllerAdvice。
以下是适用于应用程序中所有控制器端点的 @ControllerAdvice 示例:
-
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)
}
}
|
由于此 |
|
重要的是要记住,控制器端点和控制器建议是在 Spring Security 过滤器链之后被调用的。
这意味着,只有当请求通过过滤器链到达您的应用程序时,此 |
CSRF Tokens现在将可在响应头中获取(默认情况下为 X-CSRF-TOKEN 或 X-XSRF-TOKEN),适用于控制器建议所应用的所有自定义端点。
对后端的任何请求均可用于从响应中获取Tokens,随后的请求可在具有相同名称的请求头中包含该Tokens。
移动应用程序
像 JavaScript 应用程序 一样,移动应用程序通常使用 JSON 而不是 HTML。 一个不处理浏览器流量的后端应用可以选择禁用 CSRF。 在这种情况下,无需进行额外的工作。
然而,如果后端应用同时也为浏览器提供流量,因此仍然需要 CSRF 保护,则可以选择继续将 CsrfToken 存储在会话中,而不是存储在 Cookie 中。
在这种情况下,与后端集成的典型模式是暴露一个 /csrf 端点,允许前端(移动设备或浏览器客户端)按需请求 CSRF Tokens。
使用此模式的好处是,CSRF Tokens可以继续被延迟加载,仅在请求需要 CSRF 保护时才从会话中加载。
使用自定义端点还意味着客户端应用程序可以通过显式发送请求,按需(如有必要)请求生成一个新的Tokens。
|
该模式可用于任何需要 CSRF 保护的应用程序,而不仅限于移动应用程序。 尽管在这些情况下通常不需要采用这种方法,但它提供了另一种与启用了 CSRF 保护的后端进行集成的选项。 |
以下是使用 /csrf 端点的示例,该端点利用了 CsrfTokenArgumentResolver:
/csrf 端点-
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 Tokens。
|
在认证成功和登出成功后需要刷新Tokens,因为 |
获取 CSRF Tokens后,您需要自行将其作为 HTTP 请求头包含在内(默认情况下为 X-CSRF-TOKEN 或 X-XSRF-TOKEN)。
处理AccessDeniedException
要处理诸如 AccessDeniedException 这样的 InvalidCsrfTokenException,您可以配置 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
public void loginWhenInvalidCsrfTokenThenForbidden() throws Exception {
this.mockMvc.perform(post("/login").with(csrf().useInvalidToken())
.accept(MediaType.TEXT_HTML)
.param("username", "user")
.param("password", "password"))
.andExpect(status().isForbidden());
}
@Test
public void loginWhenMissingCsrfTokenThenForbidden() throws Exception {
this.mockMvc.perform(post("/login")
.accept(MediaType.TEXT_HTML)
.param("username", "user")
.param("password", "password"))
.andExpect(status().isForbidden());
}
@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
fun loginWhenInvalidCsrfTokenThenForbidden() {
mockMvc.perform(post("/login").with(csrf().useInvalidToken())
.accept(MediaType.TEXT_HTML)
.param("username", "user")
.param("password", "password"))
.andExpect(status().isForbidden)
}
@Test
fun loginWhenMissingCsrfTokenThenForbidden() {
mockMvc.perform(post("/login")
.accept(MediaType.TEXT_HTML)
.param("username", "user")
.param("password", "password"))
.andExpect(status().isForbidden)
}
@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 Tokens,从而防止恶意用户强制注销您的用户。
最简单的方法是使用表单来让用户登出。 如果你确实需要一个链接,可以使用 JavaScript 让该链接执行一个 POST 请求(例如提交一个隐藏的表单)。 对于禁用了 JavaScript 的浏览器,你可以选择让该链接将用户引导至一个登出确认页面,由该页面执行 POST 操作。
如果你确实想在注销时使用 HTTP GET,也可以这样做。
然而,请记住,这通常并不推荐。
例如,当下列配置中请求 /logout URL 时,无论使用何种 HTTP 方法都会执行注销操作:
-
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)与会话超时
默认情况下,Spring Security 使用 HttpSessionCsrfTokenRepository 将 CSRF Tokens存储在 HttpSession 中。
这可能导致会话过期的情况,从而没有可用于验证的 CSRF Tokens。
我们已经讨论过会话超时的通用解决方案。 本节将讨论与 Servlet 支持相关的 CSRF 超时的具体细节。
您可以将 CSRF Tokens的存储方式更改为使用 Cookie。
详细信息,请参阅 使用 CookieCsrfTokenRepository 部分。
如果Tokens过期,您可能希望通过指定自定义AccessDeniedHandler来自定义其处理方式。
自定义AccessDeniedHandler可以按您喜欢的方式处理InvalidCsrfTokenException。
多部分(文件上传)
我们已经讨论过,在防范CSRF攻击时保护多部分请求(文件上传)会引发一个先有鸡还是先有蛋的问题。 当JavaScript可用时,我们建议将CSRFTokens包含在HTTP请求头中,以绕开该问题。
|
您可以在 Spring 参考文档的多部分解析器部分以及 |
将 CSRF Tokens放入请求体中
我们已经讨论过将 CSRF Tokens放在请求体中的权衡问题。 在本节中,我们将讨论如何配置 Spring Security 以从请求体中读取 CSRF Tokens。
为了从请求体中读取 CSRF Tokens,需将 MultipartFilter 配置在 Spring Security 过滤器之前。
将 MultipartFilter 放在 Spring Security 过滤器之前意味着调用 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>
|
为确保在使用 XML 配置时 |
在 URL 中包含 CSRF Tokens
如果无法接受允许未授权用户上传临时文件,替代方案是将 MultipartFilter 放置在 Spring Security 过滤器之后,并将 CSRF Tokens作为查询参数包含在表单的 action 属性中。
由于 CsrfToken 被公开为 HttpServletRequest 属性,名称为 _csrf,我们可以利用它创建一个包含 CSRF Tokens的 action。
以下示例展示了如何在 JSP 中实现这一点:
<form method="post"
action="./upload?${_csrf.parameterName}=${_csrf.token}"
enctype="multipart/form-data">
隐藏 HTTP 方法过滤器
我们已经讨论过将 CSRF Tokens放在请求体中的权衡问题。
在 Spring 的 Servlet 支持中,重写 HTTP 方法是通过使用 HiddenHttpMethodFilter 实现的。
您可以在参考文档的 HTTP 方法转换 部分找到更多信息。