跨站点请求伪造 (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 中,而不是 存储在会话中 -
我想 选择退出延迟令牌
-
我需要指导将 Thymeleaf、JSP 或其他视图技术 与后端集成
-
我需要指导将 Angular 或其他 JavaScript 框架 与后端集成
-
我需要指导将 移动应用程序或其他客户端 与后端集成
-
我需要有关 处理错误 的指导
-
我想 测试 CSRF 保护
-
我需要有关 禁用 CSRF 保护 的指导
了解 CSRF 保护的组件
CSRF 保护由几个组件提供,这些组件组合在 CsrfFilter
中
CsrfFilter
组件CSRF 保护分为两个部分
-
通过委托给
CsrfTokenRequestHandler
,使应用程序能够使用CsrfToken
。 -
确定请求是否需要 CSRF 保护,加载和验证令牌,以及 处理
AccessDeniedException
。
CsrfFilter
处理-
首先,加载
DeferredCsrfToken
,它包含对CsrfTokenRepository
的引用,以便稍后(在 中)加载持久化的CsrfToken
。 -
其次,将
Supplier<CsrfToken>
(从DeferredCsrfToken
创建)提供给CsrfTokenRequestHandler
,它负责填充请求属性,使应用程序的其余部分可以使用CsrfToken
。 -
接下来,开始主要的 CSRF 保护处理,并检查当前请求是否需要 CSRF 保护。如果不需要,则继续过滤器链并结束处理。
-
如果需要 CSRF 保护,则最终会从
DeferredCsrfToken
中加载持久化的CsrfToken
。 -
继续,使用
CsrfTokenRequestHandler
解析客户端提供的实际 CSRF 令牌(如果有)。 -
将实际 CSRF 令牌与持久化的
CsrfToken
进行比较。如果有效,则继续过滤器链并结束处理。 -
如果实际 CSRF 令牌无效(或缺失),则将
AccessDeniedException
传递给AccessDeniedHandler
并结束处理。
迁移到 Spring Security 6
从 Spring Security 5 迁移到 6 时,可能会有一些更改会影响您的应用程序。以下是 Spring Security 6 中 CSRF 保护方面发生变化的概述
Spring Security 6 中的更改需要对单页应用程序进行额外的配置,因此您可能会发现 单页应用程序 部分特别有用。 |
持久化 CsrfToken
CsrfToken
使用 CsrfTokenRepository
持久化。
默认情况下,HttpSessionCsrfTokenRepository
用于将令牌存储在会话中。Spring Security 还提供 CookieCsrfTokenRepository
用于将令牌存储在 cookie 中。您也可以指定 您自己的实现 来将令牌存储在您喜欢的任何地方。
使用 HttpSessionCsrfTokenRepository
默认情况下,Spring Security 使用 HttpSessionCsrfTokenRepository
将预期的 CSRF 令牌存储在 HttpSession
中,因此不需要额外的代码。
HttpSessionCsrfTokenRepository
默认情况下从名为 X-CSRF-TOKEN
的 HTTP 请求标头或请求参数 _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
您可以将 CsrfToken
持久化到 cookie 中,以 支持基于 JavaScript 的应用程序,方法是使用 CookieCsrfTokenRepository
.
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"/>
处理 CsrfToken
CsrfToken
通过 CsrfTokenRequestHandler
提供给应用程序。此组件还负责从 HTTP 标头或请求参数中解析 CsrfToken
。
默认情况下,XorCsrfTokenRequestAttributeHandler
用于提供对 CsrfToken
的 BREACH 保护。Spring Security 还提供 CsrfTokenRequestAttributeHandler
用于选择退出 BREACH 保护。您也可以指定 您自己的实现 来自定义处理和解析令牌的策略。
使用 XorCsrfTokenRequestAttributeHandler
(BREACH)
XorCsrfTokenRequestAttributeHandler
使 CsrfToken
作为名为 _csrf
的 HttpServletRequest
属性可用,并额外提供对 BREACH 的保护。
|
此实现还从请求中解析令牌值,作为请求头(默认情况下为 X-CSRF-TOKEN
或 X-XSRF-TOKEN
)或请求参数(默认情况下为 _csrf
)。
BREACH 保护通过将随机性编码到 CSRF 令牌值中来提供,以确保返回的 |
Spring Security 默认情况下会保护 CSRF 令牌免受 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
属性可用。
|
此实现还从请求中解析令牌值,作为请求头(默认情况下为 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
接口来自定义处理和解析令牌的策略。
|
实现 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 令牌可以通过不需要在每次请求时加载会话来提高性能。
如果您想选择退出延迟令牌并使 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 保护集成
为了使 同步器令牌模式 能够防止 CSRF 攻击,我们必须在 HTTP 请求中包含实际的 CSRF 令牌。这必须包含在请求的一部分(表单参数、HTTP 标头或其他部分)中,该部分不会被浏览器自动包含在 HTTP 请求中。
以下部分描述了前端或客户端应用程序与受 CSRF 保护的后端应用程序集成的各种方式
HTML 表单
要提交 HTML 表单,必须将 CSRF 令牌作为隐藏输入包含在表单中。例如,渲染的 HTML 可能如下所示
<input type="hidden"
name="_csrf"
value="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
以下视图技术会自动将实际的 CSRF 令牌包含在具有不安全 HTTP 方法(例如 POST)的表单中
-
任何其他与
RequestDataValueProcessor
集成的视图技术(通过CsrfRequestDataValueProcessor
) -
您也可以通过 csrfInput 标签自己包含令牌
如果这些选项不可用,您可以利用以下事实:CsrfToken
作为 HttpServletRequest
属性名为 _csrf
公开。以下示例使用 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 令牌,而不是请求参数。
为了获取 CSRF 令牌,您可以配置 Spring Security 将预期的 CSRF 令牌存储 在 cookie 中。通过将预期令牌存储在 cookie 中,像 Angular 这样的 JavaScript 框架可以自动将实际 CSRF 令牌作为 HTTP 请求标头包含。
在将单页应用程序 (SPA) 与 Spring Security 的 CSRF 保护集成时,对于 BREACH 保护和延迟令牌有一些特殊注意事项。下一节提供了完整的配置示例 下一节。 |
您可以在以下部分了解不同类型的 JavaScript 应用程序
单页应用程序
将单页应用程序 (SPA) 与 Spring Security 的 CSRF 保护集成有一些特殊注意事项。
回想一下,Spring Security 默认情况下提供 CsrfToken
的 BREACH 保护。当将预期的 CSRF 令牌存储 在 cookie 中 时,JavaScript 应用程序将只能访问纯令牌值,而 *不会* 访问编码值。将需要提供一个 自定义请求处理程序 来解析实际令牌值。
此外,存储 CSRF 令牌的 cookie 将在身份验证成功和注销成功后被清除。Spring Security 默认情况下会延迟加载新的 CSRF 令牌,并且需要额外的操作才能返回一个新的 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 令牌是 HTTP 请求头 (X-XSRF-TOKEN ) 还是请求参数 (_csrf ) 来解析 CSRF 令牌。 |
3 | 配置一个自定义的 Filter ,以便在每个请求上加载 CsrfToken ,如果需要,它将返回一个新的 cookie。 |
多页面应用程序
对于在每个页面上加载 JavaScript 的多页面应用程序,除了将 CSRF 令牌暴露在 cookie 中 之外,还可以将 CSRF 令牌包含在您的 meta
标签中。HTML 可能看起来像这样
<html>
<head>
<meta name="_csrf" content="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
<meta name="_csrf_header" content="X-CSRF-TOKEN"/>
<!-- ... -->
</head>
<!-- ... -->
</html>
为了在请求中包含 CSRF 令牌,您可以利用 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>
一旦元标签包含了 CSRF 令牌,JavaScript 代码就可以读取元标签并将 CSRF 令牌作为头信息包含在内。如果您使用 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
的示例
-
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 令牌将在响应头中可用(默认情况下为 X-CSRF-TOKEN
或 X-XSRF-TOKEN
),用于控制器建议适用的任何自定义端点。任何对后端的请求都可以用来从响应中获取令牌,并且后续请求可以在具有相同名称的请求头中包含该令牌。
移动应用程序
与 JavaScript 应用程序 类似,移动应用程序通常使用 JSON 而不是 HTML。不提供浏览器流量的后台应用程序可以选择 禁用 CSRF。在这种情况下,不需要额外的操作。
但是,一个也提供浏览器流量的后台应用程序,因此仍然需要 CSRF 保护,可以继续将 CsrfToken
存储在会话中,而不是 存储在 Cookie 中。
在这种情况下,与后台集成的一种典型模式是公开一个 /csrf
端点,允许前端(移动或浏览器客户端)按需请求 CSRF 令牌。使用这种模式的好处是 CSRF 令牌 可以继续延迟,并且只有在请求需要 CSRF 保护时才需要从会话中加载。使用自定义端点也意味着客户端应用程序可以请求按需生成新的令牌(如果需要)通过发出显式请求。
这种模式可以用于任何需要 CSRF 保护的应用程序,而不仅仅是移动应用程序。虽然这种方法在这些情况下通常不需要,但它是与 CSRF 保护的后台集成的另一种选择。 |
以下是使用 CsrfTokenArgumentResolver
的 /csrf
端点的示例
/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 令牌。
在身份验证成功和注销成功后刷新令牌是必需的,因为 |
获得 CSRF 令牌后,您需要将其作为 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 令牌,并且恶意用户无法强制注销您的用户。
最简单的方法是使用表单注销用户。如果您确实想要一个链接,可以使用 JavaScript 让链接执行 POST(可能是在隐藏表单上)。对于禁用 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 和会话超时
默认情况下,Spring Security 使用HttpSessionCsrfTokenRepository
将 CSRF 令牌存储在HttpSession
中。这会导致会话过期的情况,从而导致没有 CSRF 令牌可供验证。
我们已经讨论了会话超时的通用解决方案。本节讨论与 servlet 支持相关的 CSRF 超时的具体情况。
您可以将 CSRF 令牌的存储更改为 cookie。有关详细信息,请参阅使用CookieCsrfTokenRepository
部分。
如果令牌确实过期,您可能希望通过指定自定义AccessDeniedHandler
来定制其处理方式。自定义AccessDeniedHandler
可以按您喜欢的方式处理InvalidCsrfTokenException
。
多部分(文件上传)
我们已经讨论过如何保护多部分请求(文件上传)免受 CSRF 攻击会导致先有鸡还是先有蛋的问题。当 JavaScript 可用时,我们建议在 HTTP 请求标头中包含 CSRF 令牌以避免此问题。
您可以在 Spring 参考的多部分解析器部分和 |
将 CSRF 令牌放置在主体中
我们已经讨论过将 CSRF 令牌放置在主体中的权衡。在本节中,我们将讨论如何配置 Spring Security 以从主体读取 CSRF。
为了从主体中读取 CSRF 令牌,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>
为了确保 |
在 URL 中包含 CSRF 令牌
如果不允许未经授权的用户上传临时文件,则另一种方法是在 Spring Security 过滤器之后放置 MultipartFilter
,并将 CSRF 作为查询参数包含在表单的 action 属性中。由于 CsrfToken
作为 名为 _csrf
的 HttpServletRequest
属性 公开,我们可以使用它来创建包含 CSRF 令牌的 action
。以下示例使用 JSP 来完成此操作
<form method="post"
action="./upload?${_csrf.parameterName}=${_csrf.token}"
enctype="multipart/form-data">
HiddenHttpMethodFilter
我们已经讨论了将 CSRF 令牌放置在主体中的权衡取舍。
在 Spring 的 Servlet 支持中,使用 HiddenHttpMethodFilter
来覆盖 HTTP 方法。您可以在参考文档的 HTTP 方法转换 部分找到更多信息。