跨站请求伪造 (CSRF)

在最终用户可以登录的应用程序中,考虑如何防范跨站请求伪造 (CSRF) 是非常重要的。

Spring Security 默认情况下会保护不安全的 HTTP 方法(例如 POST 请求)免受 CSRF 攻击,因此无需额外代码。您可以使用以下配置明确指定默认配置:

配置 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 保护的组件

CSRF 保护由CsrfFilter 中组合的几个组件提供

csrf
图 1. CsrfFilter 组件

CSRF 保护分为两部分:

  1. 通过委托给CsrfTokenRequestHandler,使CsrfToken 可用于应用程序。

  2. 确定请求是否需要 CSRF 保护,加载并验证令牌,并处理 AccessDeniedException

csrf processing
图 2. CsrfFilter 处理流程
  • number 1 首先,加载DeferredCsrfToken,它持有对CsrfTokenRepository的引用,以便稍后(在number 4中)加载持久化的CsrfToken

  • number 2 其次,将 Supplier<CsrfToken>(从 DeferredCsrfToken 创建)提供给CsrfTokenRequestHandler,后者负责填充请求属性,使 CsrfToken 可用于应用程序的其余部分。

  • number 3 接下来,主要的 CSRF 保护处理开始,并检查当前请求是否需要 CSRF 保护。如果不需要,则继续过滤器链并结束处理。

  • number 4 如果需要 CSRF 保护,则最终从 DeferredCsrfToken 加载持久化的 CsrfToken

  • number 5 继续,客户端提供的实际 CSRF 令牌(如果有)使用CsrfTokenRequestHandler进行解析。

  • number 6 实际的 CSRF 令牌与持久化的 CsrfToken 进行比较。如果有效,则继续过滤器链并结束处理。

  • number 7 如果实际的 CSRF 令牌无效(或缺失),则会将 AccessDeniedException 传递给AccessDeniedHandler,并结束处理。

迁移到 Spring Security 6

从 Spring Security 5 迁移到 6 时,有一些更改可能会影响您的应用程序。以下是 Spring Security 6 中 CSRF 保护方面发生变化的概述:

Spring Security 6 中的更改要求单页应用程序进行额外配置,因此您可能会发现单页应用程序部分特别有用。

有关迁移 Spring Security 5 应用程序的更多信息,请参阅迁移章节的漏洞保护部分。

持久化 CsrfToken

CsrfToken 使用 CsrfTokenRepository 进行持久化。

默认情况下,HttpSessionCsrfTokenRepository 用于在会话中存储令牌。Spring Security 还提供CookieCsrfTokenRepository 用于在 cookie 中存储令牌。您还可以指定自己的实现来将令牌存储在任何您喜欢的位置。

使用 HttpSessionCsrfTokenRepository

默认情况下,Spring Security 通过使用HttpSessionCsrfTokenRepository 将预期的 CSRF 令牌存储在 HttpSession 中,因此无需额外代码。

HttpSessionCsrfTokenRepository 从会话(无论是内存、缓存还是数据库)中读取令牌。如果您需要直接访问会话属性,请首先使用 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"/>

您可以使用CookieCsrfTokenRepositoryCsrfToken 持久化到 cookie 中,以支持基于 JavaScript 的应用程序

默认情况下,CookieCsrfTokenRepository 将数据写入名为 XSRF-TOKEN 的 cookie,并从名为 X-XSRF-TOKEN 的 HTTP 请求头或请求参数 _csrf 中读取。这些默认值来源于 Angular 及其前身 AngularJS

有关此主题的最新信息,请参阅HttpClient XSRF/CSRF 安全性withXsrfConfiguration

您可以使用以下配置来配置 CookieCsrfTokenRepository

该示例明确将 HttpOnly 设置为 false。这是为了让 JavaScript 框架(如 Angular)能够读取它。如果您不需要直接使用 JavaScript 读取 cookie 的能力,我们**建议**省略 HttpOnly(通过使用 new 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 用于提供 CsrfTokenBREACH 保护。Spring Security 还提供CsrfTokenRequestAttributeHandler 用于选择退出 BREACH 保护。您还可以指定自己的实现来定制处理和解析令牌的策略。

使用 XorCsrfTokenRequestAttributeHandler (BREACH)

XorCsrfTokenRequestAttributeHandlerCsrfToken 作为名为 _csrfHttpServletRequest 属性提供,并额外提供对 BREACH 的保护。

CsrfToken 也作为请求属性提供,使用名称 CsrfToken.class.getName()。此名称不可配置,但可以使用 XorCsrfTokenRequestAttributeHandler#setCsrfRequestAttributeName 更改名称 _csrf

此实现还从请求中解析令牌值,可以是请求头(默认为X-CSRF-TOKENX-XSRF-TOKEN之一)或请求参数(默认为_csrf)。

通过将随机性编码到 CSRF 令牌值中以确保返回的 CsrfToken 在每次请求时都会更改,从而提供 BREACH 保护。当令牌随后被解析为头部值或请求参数时,它会被解码以获取原始令牌,然后将其与持久化的 CsrfToken进行比较。

Spring Security 默认情况下保护 CSRF 令牌免受 BREACH 攻击,因此无需额外代码。您可以使用以下配置明确指定默认配置:

配置 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

CsrfTokenRequestAttributeHandlerCsrfToken 作为名为 _csrfHttpServletRequest 属性提供。

CsrfToken 也作为请求属性提供,使用名称 CsrfToken.class.getName()。此名称不可配置,但可以使用 CsrfTokenRequestAttributeHandler#setCsrfRequestAttributeName 更改名称 _csrf

此实现还从请求中解析令牌值,可以是请求头(默认为X-CSRF-TOKENX-XSRF-TOKEN之一)或请求参数(默认为_csrf)。

CsrfTokenRequestAttributeHandler 的主要用途是选择退出 CsrfToken 的 BREACH 保护,可以通过以下配置进行配置:

选择退出 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 接口是一个 @FunctionalInterface,可以使用 lambda 表达式实现以自定义请求处理。您需要实现完整的接口以自定义如何从请求中解析令牌。请参阅为单页应用程序配置 CSRF,了解一个使用委托来实现处理和解析令牌的自定义策略的示例。

实现 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)发出请求时,需要 CsrfToken。此外,任何将令牌渲染到响应中的请求(例如包含用于 CSRF 令牌的隐藏 <input><form> 标签的网页)也需要它。

由于 Spring Security 默认还将 CsrfToken 存储在 HttpSession 中,因此延迟的 CSRF 令牌可以通过不需要在每个请求上加载会话来提高性能。

如果您想选择退出延迟令牌并使 CsrfToken 在每个请求上加载,您可以使用以下配置:

选择退出延迟 CSRF 令牌
  • 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>

通过将 csrfRequestAttributeName 设置为 null,必须首先加载 CsrfToken 以确定要使用的属性名称。这会导致 CsrfToken 在每个请求上加载。

与 CSRF 保护集成

为了让同步器令牌模式保护应用程序免受 CSRF 攻击,我们必须在 HTTP 请求中包含实际的 CSRF 令牌。这必须包含在请求的某个部分(表单参数、HTTP 头或其他部分),且该部分不会由浏览器自动包含在 HTTP 请求中。

以下部分描述了前端或客户端应用程序与受 CSRF 保护的后端应用程序集成的各种方式:

HTML 表单

要提交 HTML 表单,必须在表单中包含 CSRF 令牌作为隐藏输入。例如,渲染的 HTML 可能看起来像这样:

HTML 表单中的 CSRF 令牌
<input type="hidden"
	name="_csrf"
	value="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>

以下视图技术会自动将实际的 CSRF 令牌包含在具有不安全 HTTP 方法(例如 POST)的表单中:

如果这些选项不可用,您可以利用 CsrfToken 作为名为 _csrfHttpServletRequest 属性公开的事实。以下示例使用 JSP 完成此操作:

使用请求属性在 HTML 表单中包含 CSRF 令牌
<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。

在认证成功和注销成功后刷新令牌是必需的,因为CsrfAuthenticationStrategyCsrfLogoutHandler将清除之前的令牌。客户端应用程序将无法在未获得新令牌的情况下执行不安全的 HTTP 请求,例如 POST。

为了方便地将单页应用程序与 Spring Security 集成,可以使用以下配置:

为单页应用程序配置 CSRF
  • Java

  • Kotlin

  • XML

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			// ...
			.csrf((csrf) -> csrf.spa());
		return http.build();
	}
}
import org.springframework.security.config.annotation.web.invoke

@Configuration
@EnableWebSecurity
class SecurityConfig {

    @Bean
    open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            // ...
            csrf {
                spa()
            }
        }
        return http.build()
    }
}
<http>
	<!-- ... -->
	<csrf>
        <spa />
    </csrf>
</http>

多页应用程序

对于 JavaScript 在每个页面上加载的多页应用程序,除了将 CSRF 令牌存储在 cookie 中之外,另一种选择是将其包含在 meta 标签中。HTML 可能看起来像这样:

HTML Meta 标签中的 CSRF 令牌
<html>
<head>
	<meta name="_csrf" content="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
	<meta name="_csrf_header" content="X-CSRF-TOKEN"/>
	<!-- ... -->
</head>
<!-- ... -->
</html>

为了将 CSRF 令牌包含在请求中,您可以利用 CsrfToken 作为名为 _csrfHttpServletRequest 属性公开的事实。以下示例使用 JSP 完成此操作:

使用请求属性在 HTML Meta 标签中包含 CSRF 令牌
<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 令牌,JavaScript 代码就可以读取 meta 标签并将 CSRF 令牌作为头信息包含进来。如果您使用 jQuery,可以使用以下代码完成:

在 AJAX 请求中包含 CSRF 令牌
$(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 令牌包含在 HTTP 响应头中。

实现此目的的一种方法是通过使用 @ControllerAdviceCsrfTokenArgumentResolver。以下是适用于应用程序中所有控制器端点的 @ControllerAdvice 示例:

HTTP 响应头中的 CSRF 令牌
  • 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)
	}

}

由于此 @ControllerAdvice 适用于应用程序中的所有端点,它将导致 CSRF 令牌在每个请求上加载,这在使用HttpSessionCsrfTokenRepository时可能会抵消延迟令牌的优势。但是,在使用CookieCsrfTokenRepository时,这通常不是问题。

需要记住的是,控制器端点和控制器通知在 Spring Security 过滤器链**之后**被调用。这意味着此 @ControllerAdvice 仅在请求通过过滤器链到达您的应用程序时才会被应用。请参阅单页应用程序的配置,了解在过滤器链中添加过滤器以更早访问 HttpServletResponse 的示例。

CSRF 令牌现在将在响应头中可用(默认为X-CSRF-TOKENX-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
    }

}

如果上述端点需要在与服务器进行身份验证之前,您可以考虑添加 .requestMatchers("/csrf").permitAll()

当应用程序启动或初始化时(例如在加载时),以及在认证成功和注销成功后,都应调用此端点以获取 CSRF 令牌。

在认证成功和注销成功后刷新令牌是必需的,因为CsrfAuthenticationStrategyCsrfLogoutHandler将清除之前的令牌。客户端应用程序将无法在未获得新令牌的情况下执行不安全的 HTTP 请求,例如 POST。

一旦您获得了 CSRF 令牌,您需要自己将其作为 HTTP 请求头(默认为X-CSRF-TOKENX-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 保护,如下所示:

测试 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.config.http.PathPatternRequestMatcherFactoryBean">
                <b:constructor-arg value="/api/*"/>
            </b:bean>
        </b:bean>
    </b:constructor-arg>
</b:bean>

如果您需要禁用 CSRF 保护,可以使用以下配置:

禁用 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 保护以防止伪造登录尝试非常重要要求登录请求进行 CSRF 保护。Spring Security 的 Servlet 支持开箱即用。

登出

为了保护免受伪造的注销尝试,要求注销请求进行 CSRF 保护是很重要的。如果启用了 CSRF 保护(默认),Spring Security 的 LogoutFilter 将只处理 HTTP POST 请求。这确保了注销需要 CSRF 令牌,并且恶意用户无法强制您的用户注销。

最简单的方法是使用表单让用户注销。如果您真的想要一个链接,可以使用 JavaScript 让链接执行 POST 操作(可能是在一个隐藏表单上)。对于禁用 JavaScript 的浏览器,您可以选择让链接将用户带到一个执行 POST 操作的注销确认页面。

如果你真的想用 HTTP GET 来注销,你可以这样做。但是,请记住,这通常不推荐。例如,当使用任何 HTTP 方法请求 /logout URL 时,以下代码会注销:

使用任何 HTTP 方法注销
  • Java

  • Kotlin

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			// ...
			.logout((logout) -> logout
				.logoutRequestMatcher(PathPatternRequestMatcher.withDefaults().matcher("/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 = PathPatternRequestMatcher.withDefaults().matcher("/logout")
            }
        }
        return http.build()
    }
}

有关更多信息,请参阅注销章节。

CSRF 和会话超时

默认情况下,Spring Security 使用HttpSessionCsrfTokenRepository将 CSRF 令牌存储在 HttpSession 中。这可能导致会话过期,从而没有 CSRF 令牌可供验证的情况。

我们已经讨论了会话超时的通用解决方案。本节讨论 CSRF 超时在 Servlet 支持方面的具体内容。

您可以将 CSRF 令牌的存储更改为 cookie。有关详细信息,请参阅使用 CookieCsrfTokenRepository部分。

如果令牌确实过期了,您可能希望通过指定自定义 AccessDeniedHandler来定制其处理方式。自定义的 AccessDeniedHandler 可以以您喜欢的任何方式处理 InvalidCsrfTokenException

多部分(文件上传)

我们已经讨论过如何保护多部分请求(文件上传)免受 CSRF 攻击会导致先有鸡还是先有蛋的问题。当 JavaScript 可用时,我们**建议**在 HTTP 请求头中包含 CSRF 令牌以避免这个问题。

如果 JavaScript 不可用,以下部分讨论了在 Servlet 应用程序中将 CSRF 令牌放置在主体URL中的选项。

您可以在 Spring 参考文档的多部分解析器部分和MultipartFilter javadoc中找到有关将多部分表单与 Spring 一起使用的更多信息。

将 CSRF 令牌放置在请求体中

我们已经讨论过将 CSRF 令牌放置在请求体中的权衡。在本节中,我们讨论如何配置 Spring Security 以从请求体中读取 CSRF。

为了从请求体中读取 CSRF 令牌,MultipartFilter 在 Spring Security 过滤器之前指定。在 Spring Security 过滤器之前指定 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>

为了确保 MultipartFilter 在 XML 配置中位于 Spring Security 过滤器之前,您可以确保 MultipartFilter<filter-mapping> 元素放置在 web.xml 文件中的 springSecurityFilterChain 之前。

在 URL 中包含 CSRF 令牌

如果允许未经授权的用户上传临时文件是不可接受的,另一种选择是将 MultipartFilter 放在 Spring Security 过滤器之后,并将 CSRF 作为查询参数包含在表单的 action 属性中。由于 CsrfToken 作为名为 _csrfHttpServletRequest 属性公开,我们可以使用它来创建一个包含 CSRF 令牌的 action。以下示例使用 JSP 完成此操作:

Action 中的 CSRF 令牌
<form method="post"
	action="./upload?${_csrf.parameterName}=${_csrf.token}"
	enctype="multipart/form-data">

HiddenHttpMethodFilter

我们已经讨论过将 CSRF 令牌放置在请求体中的权衡。

在 Spring 的 Servlet 支持中,覆盖 HTTP 方法是通过使用HiddenHttpMethodFilter完成的。您可以在参考文档的HTTP 方法转换部分找到更多信息。

延伸阅读

既然您已经了解了 CSRF 保护,那么可以考虑学习更多关于漏洞保护的知识,包括安全头HTTP 防火墙,或者继续学习如何测试您的应用程序。

© . This site is unofficial and not affiliated with VMware.