跨站请求伪造(CSRF)

在一个允许最终用户登录的应用程序中,考虑如何防范跨站请求伪造(CSRF)非常重要。spring-doc.cadn.net.cn

Spring Security 默认会对不安全的 HTTP 方法(例如 POST 请求)启用 CSRF 攻击防护,因此无需额外编写代码。 你可以通过以下方式显式指定默认配置:spring-doc.cadn.net.cn

配置 CSRF 保护
@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 防护信息,请考虑以下使用场景:spring-doc.cadn.net.cn

了解 CSRF 保护的组件

CSRF 保护由多个组件提供,这些组件组合在 CsrfFilter 中:spring-doc.cadn.net.cn

csrf
图1. CsrfFilter 组件

CSRF 保护分为两个部分:spring-doc.cadn.net.cn

  1. 通过委托给 CsrfTokenRequestHandler,使 CsrfToken 对应用程序可用。spring-doc.cadn.net.cn

  2. 确定请求是否需要 CSRF 保护,加载并验证Tokens,然后 处理 AccessDeniedExceptionspring-doc.cadn.net.cn

csrf processing
图2. CsrfFilter 处理流程

迁移到 Spring Security 6

从 Spring Security 5 迁移到 6 时,有一些变更可能会影响您的应用程序。 以下是 Spring Security 6 中 CSRF 保护相关变更的概述:spring-doc.cadn.net.cn

Spring Security 6 的变更要求为单页应用程序进行额外的配置,因此您可能会发现单页应用程序部分特别有用。spring-doc.cadn.net.cn

有关迁移 Spring Security 5 应用程序的更多信息,请参阅迁移章节中的漏洞防护部分。spring-doc.cadn.net.cn

持久化CsrfToken

CsrfToken 使用 CsrfTokenRepository 进行持久化。spring-doc.cadn.net.cn

默认情况下,HttpSessionCsrfTokenRepository 用于在会话中存储Tokens。 Spring Security 还提供了 CookieCsrfTokenRepository 用于在 Cookie 中存储Tokens。 您还可以指定 您自己的实现,以便将Tokens存储在您喜欢的任何位置。spring-doc.cadn.net.cn

使用HttpSessionCsrfTokenRepository

默认情况下,Spring Security 使用 HttpSessionCsrfTokenRepository 将预期的 CSRF Tokens存储在 HttpSession 中,因此无需额外代码。spring-doc.cadn.net.cn

HttpSessionCsrfTokenRepository 从会话(无论是内存、缓存还是数据库中的会话)中读取Tokens。如果您需要直接访问会话属性,请首先使用 HttpSessionCsrfTokenRepository#setSessionAttributeName 配置会话属性名称。spring-doc.cadn.net.cn

你可以使用以下配置显式指定默认配置:spring-doc.cadn.net.cn

配置 HttpSessionCsrfTokenRepository
@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"/>

您可以将 CsrfToken 持久化到 Cookie 中,以使用 CookieCsrfTokenRepository 支持基于 JavaScript 的应用程序 spring-doc.cadn.net.cn

CookieCsrfTokenRepository 默认会将 CSRF Tokens写入名为 XSRF-TOKEN 的 Cookie 中,并从名为 X-XSRF-TOKEN 的 HTTP 请求头或名为 _csrf 的请求参数中读取该Tokens。 这些默认设置源自 Angular 及其前身 AngularJSspring-doc.cadn.net.cn

有关此主题的最新信息,请参阅HttpClient XSRF/CSRF 安全性withXsrfConfigurationspring-doc.cadn.net.cn

您可以使用以下配置来设置 CookieCsrfTokenRepositoryspring-doc.cadn.net.cn

该示例显式地将 HttpOnly 设置为 false。 这是为了让 JavaScript 框架(例如 Angular)能够读取该 Cookie。 如果您不需要通过 JavaScript 直接读取该 Cookie,我们建议省略 HttpOnly(即改用 new CookieCsrfTokenRepository())以提高安全性。spring-doc.cadn.net.cn

自定义CsrfTokenRepository

在某些情况下,您可能需要实现自定义的 CsrfTokenRepositoryspring-doc.cadn.net.cn

一旦你实现了 CsrfTokenRepository 接口,就可以通过以下配置让 Spring Security 使用它:spring-doc.cadn.net.cn

配置自定义 CsrfTokenRepository
@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 头部或请求参数中解析 CsrfTokenspring-doc.cadn.net.cn

默认情况下,使用 XorCsrfTokenRequestAttributeHandler 来提供 BREACH 防护,以保护 CsrfToken。 Spring Security 还提供了 CsrfTokenRequestAttributeHandler,用于选择退出 BREACH 防护。 您也可以指定 自己的实现,以自定义处理和解析Tokens的策略。spring-doc.cadn.net.cn

使用XorCsrfTokenRequestAttributeHandler(入侵)

XorCsrfTokenRequestAttributeHandlerCsrfToken 作为名为 HttpServletRequest_csrf 属性提供,并额外提供了针对 BREACH 攻击的防护。spring-doc.cadn.net.cn

CsrfToken 也会以请求属性的形式提供,其名称为 CsrfToken.class.getName()。 该名称不可配置,但名称 _csrf 可通过 XorCsrfTokenRequestAttributeHandler#setCsrfRequestAttributeName 进行修改。spring-doc.cadn.net.cn

此实现还会从请求中解析Tokens值,作为请求头(默认情况下为 X-CSRF-TOKENX-XSRF-TOKEN)或请求参数(默认为 _csrf)。spring-doc.cadn.net.cn

BREACH 防护通过随机性编码到 CSRF Tokens值中来实现,以确保每次请求返回的 CsrfToken 都会发生变化。 当该Tokens随后作为头部值或请求参数被解析时,它会被解码以获取原始Tokens,然后将其与 持久化的 CsrfToken 进行比较。spring-doc.cadn.net.cn

Spring Security 默认会保护 CSRF Tokens免受 BREACH 攻击,因此无需额外的代码。 你可以通过以下配置显式指定默认配置:spring-doc.cadn.net.cn

配置 BREACH 防护
@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 作为名为 HttpServletRequest_csrf 属性提供。spring-doc.cadn.net.cn

CsrfToken 也会以请求属性的形式提供,其名称为 CsrfToken.class.getName()。 该名称不可配置,但名称 _csrf 可通过 CsrfTokenRequestAttributeHandler#setCsrfRequestAttributeName 进行修改。spring-doc.cadn.net.cn

此实现还会从请求中解析Tokens值,作为请求头(默认情况下为 X-CSRF-TOKENX-XSRF-TOKEN)或请求参数(默认为 _csrf)。spring-doc.cadn.net.cn

CsrfTokenRequestAttributeHandler 的主要用途是选择退出对 CsrfToken 的 BREACH 防护,这可以通过以下配置进行设置:spring-doc.cadn.net.cn

选择退出 BREACH 防护
@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的策略。spring-doc.cadn.net.cn

CsrfTokenRequestHandler 接口是一个 @FunctionalInterface,可以通过 lambda 表达式实现,以自定义请求处理方式。 若要自定义如何从请求中解析Tokens,您需要实现该接口的全部方法。 有关使用委托来实现处理和解析Tokens的自定义策略的示例,请参阅为单页应用程序配置 CSRFspring-doc.cadn.net.cn

一旦你实现了 CsrfTokenRequestHandler 接口,就可以通过以下配置让 Spring Security 使用它:spring-doc.cadn.net.cn

配置自定义 CsrfTokenRequestHandler
@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,直到需要时才加载。spring-doc.cadn.net.cn

每当使用不安全的 HTTP 方法(例如 POST)发起请求时,都需要 ../../features/exploits/csrf.html#csrf-protection-read-only。 此外,任何需要在响应中渲染该Tokens的请求也需要它,例如包含一个隐藏 <form> 字段用于 CSRF Tokens的 <input> 标签的网页。spring-doc.cadn.net.cn

由于 Spring Security 默认也将 CsrfToken 存储在 HttpSession 中,因此延迟加载 CSRF Tokens可以通过避免在每个请求中都加载会话来提升性能。spring-doc.cadn.net.cn

如果你希望退出延迟加载Tokens,并在每个请求中都加载 CsrfToken,可以通过以下配置实现:spring-doc.cadn.net.cn

退出延迟 CSRF Tokens
@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 以确定要使用的属性名。 这会导致在每个请求上都加载 CsrfTokenspring-doc.cadn.net.cn

与 CSRF 保护集成

为了使用同步Tokens模式来防范 CSRF 攻击,我们必须在 HTTP 请求中包含实际的 CSRF Tokens。 该Tokens必须包含在请求的某个部分(例如表单参数、HTTP 头部或其他部分)中,而这一部分不能由浏览器自动包含在 HTTP 请求中。spring-doc.cadn.net.cn

以下各节介绍了前端或客户端应用程序与启用了 CSRF 保护的后端应用程序进行集成的各种方式:spring-doc.cadn.net.cn

HTML 表单

要提交一个 HTML 表单,CSRF Tokens必须作为隐藏输入字段包含在表单中。 例如,渲染后的 HTML 可能如下所示:spring-doc.cadn.net.cn

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

以下视图技术会自动在使用不安全 HTTP 方法(例如 POST)的表单中包含实际的 CSRF Tokens:spring-doc.cadn.net.cn

如果这些选项不可用,您可以利用这样一个事实:CsrfToken 被公开为一个名为 _csrfHttpServletRequest 属性。 以下示例使用 JSP 实现了这一点:spring-doc.cadn.net.cn

在 HTML 表单中通过请求属性使用 CSRF Tokens
<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 请求头中提交,而不是作为请求参数。spring-doc.cadn.net.cn

为了获取 CSRF Tokens,您可以配置 Spring Security 将预期的 CSRF Tokens存储在 Cookie 中。 通过将预期的Tokens存储在 Cookie 中,诸如Angular之类的 JavaScript 框架可以自动将实际的 CSRF Tokens作为 HTTP 请求头包含进去。spring-doc.cadn.net.cn

在将单页应用(SPA)与 Spring Security 的 CSRF 保护集成时,对于 BREACH 防护和延迟Tokens(deferred tokens)有一些特殊注意事项。 完整的配置示例请参见下一节spring-doc.cadn.net.cn

您可以在以下章节中了解不同类型的 JavaScript 应用程序:spring-doc.cadn.net.cn

单页应用程序

将单页应用(SPA)与 Spring Security 的 CSRF 保护机制集成时,有一些特殊注意事项。spring-doc.cadn.net.cn

请记住,Spring Security 默认提供CsrfToken的 BREACH 防护。 当将预期的 CSRF Tokens存储为cookie时,JavaScript 应用程序仅能访问纯文本Tokens值,而无法访问编码后的值。 因此,需要提供一个用于解析实际Tokens值的自定义请求处理器spring-doc.cadn.net.cn

此外,在认证成功和注销成功时,用于存储 CSRF Tokens的 Cookie 将被清除。 Spring Security 默认会延迟加载新的 CSRF Tokens,若要返回一个新的 Cookie,需要进行额外的处理。spring-doc.cadn.net.cn

在认证成功和登出成功后需要刷新Tokens,因为 CsrfAuthenticationStrategyCsrfLogoutHandler 会清除之前的Tokens。 客户端应用程序将无法执行不安全的 HTTP 请求(例如 POST),除非获取新的Tokens。spring-doc.cadn.net.cn

为了轻松地将单页应用程序与 Spring Security 集成,可以使用以下配置:spring-doc.cadn.net.cn

为单页应用程序配置 CSRF
@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 Tokens暴露在 Cookie 中之外,另一种方法是将 CSRF Tokens包含在您的 meta 标签中。 HTML 代码可能如下所示:spring-doc.cadn.net.cn

HTML Meta 标签中的 CSRF Tokens
<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 中实现这一点:spring-doc.cadn.net.cn

在 HTML Meta 标签中使用请求属性的 CSRF Tokens
<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,可以通过以下代码实现:spring-doc.cadn.net.cn

在 AJAX 请求中包含 CSRF Tokens
$(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 响应头中。spring-doc.cadn.net.cn

一种实现方法是使用带有 CsrfTokenArgumentResolver@ControllerAdvice。 以下是适用于应用程序中所有控制器端点的 @ControllerAdvice 示例:spring-doc.cadn.net.cn

HTTP 响应头中的 CSRF Tokens
@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 Tokens在每次请求时加载,这可能会在使用 HttpSessionCsrfTokenRepository 时抵消 延迟Tokens 的优势。 然而,在使用 CookieCsrfTokenRepository 时,这通常不是问题。spring-doc.cadn.net.cn

重要的是要记住,控制器端点和控制器建议是在 Spring Security 过滤器链之后被调用的。 这意味着,只有当请求通过过滤器链到达您的应用程序时,此@ControllerAdvice才会生效。 有关为过滤器链添加过滤器以更早访问HttpServletResponse的示例,请参阅单页应用程序的配置。spring-doc.cadn.net.cn

CSRF Tokens现在将可在响应头中获取(默认情况下为 X-CSRF-TOKENX-XSRF-TOKEN),适用于控制器建议所应用的所有自定义端点。 对后端的任何请求均可用于从响应中获取Tokens,随后的请求可在具有相同名称的请求头中包含该Tokens。spring-doc.cadn.net.cn

移动应用程序

JavaScript 应用程序 一样,移动应用程序通常使用 JSON 而不是 HTML。 一个处理浏览器流量的后端应用可以选择禁用 CSRF。 在这种情况下,无需进行额外的工作。spring-doc.cadn.net.cn

然而,如果后端应用同时也为浏览器提供流量,因此仍然需要 CSRF 保护,则可以选择继续将 CsrfToken 存储在会话中,而不是存储在 Cookie 中spring-doc.cadn.net.cn

在这种情况下,与后端集成的典型模式是暴露一个 /csrf 端点,允许前端(移动设备或浏览器客户端)按需请求 CSRF Tokens。 使用此模式的好处是,CSRF Tokens可以继续被延迟加载,仅在请求需要 CSRF 保护时才从会话中加载。 使用自定义端点还意味着客户端应用程序可以通过显式发送请求,按需(如有必要)请求生成一个新的Tokens。spring-doc.cadn.net.cn

该模式可用于任何需要 CSRF 保护的应用程序,而不仅限于移动应用程序。 尽管在这些情况下通常不需要采用这种方法,但它提供了另一种与启用了 CSRF 保护的后端进行集成的选项。spring-doc.cadn.net.cn

以下是使用 /csrf 端点的示例,该端点利用了 CsrfTokenArgumentResolverspring-doc.cadn.net.cn

/csrf 端点
@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()spring-doc.cadn.net.cn

应在应用程序启动或初始化时(例如在加载时),以及在认证成功和注销成功之后调用此端点以获取 CSRF Tokens。spring-doc.cadn.net.cn

在认证成功和登出成功后需要刷新Tokens,因为 CsrfAuthenticationStrategyCsrfLogoutHandler 会清除之前的Tokens。 客户端应用程序将无法执行不安全的 HTTP 请求(例如 POST),除非获取新的Tokens。spring-doc.cadn.net.cn

获取 CSRF Tokens后,您需要自行将其作为 HTTP 请求头包含在内(默认情况下为 X-CSRF-TOKENX-XSRF-TOKEN)。spring-doc.cadn.net.cn

处理AccessDeniedException

要处理诸如 AccessDeniedException 这样的 InvalidCsrfTokenException,您可以配置 Spring Security 以任意方式处理这些异常。 例如,您可以使用以下配置来设置一个自定义的访问拒绝页面:spring-doc.cadn.net.cn

配置 AccessDeniedHandler
@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 保护,示例如下:spring-doc.cadn.net.cn

测试 CSRF 保护
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 保护之前,请考虑它是否对您的应用程序有意义spring-doc.cadn.net.cn

你也可以考虑是否仅某些端点不需要 CSRF 保护,并配置忽略规则,如下例所示:spring-doc.cadn.net.cn

忽略请求
@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 保护,可以使用以下配置来实现:spring-doc.cadn.net.cn

禁用 CSRF
@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 注意事项spring-doc.cadn.net.cn

登录

要求对登录请求启用 CSRF 防护以防止伪造登录尝试,这一点非常重要。 Spring Security 的 Servlet 支持默认已自动实现此功能。spring-doc.cadn.net.cn

正在退出

要求登出请求启用 CSRF 防护非常重要,以防止伪造登出操作。 如果启用了 CSRF 保护(默认情况下已启用),Spring Security 的 LogoutFilter 将仅处理 HTTP POST 请求。 这确保了登出操作需要 CSRF Tokens,从而防止恶意用户强制注销您的用户。spring-doc.cadn.net.cn

最简单的方法是使用表单来让用户登出。 如果你确实需要一个链接,可以使用 JavaScript 让该链接执行一个 POST 请求(例如提交一个隐藏的表单)。 对于禁用了 JavaScript 的浏览器,你可以选择让该链接将用户引导至一个登出确认页面,由该页面执行 POST 操作。spring-doc.cadn.net.cn

如果你确实想在注销时使用 HTTP GET,也可以这样做。 然而,请记住,这通常并不推荐。 例如,当下列配置中请求 /logout URL 时,无论使用何种 HTTP 方法都会执行注销操作:spring-doc.cadn.net.cn

使用任意 HTTP 方法注销
@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()
    }
}

有关更多信息,请参阅注销章节。spring-doc.cadn.net.cn

跨站请求伪造(CSRF)与会话超时

默认情况下,Spring Security 使用 HttpSessionCsrfTokenRepository 将 CSRF Tokens存储在 HttpSession 中。 这可能导致会话过期的情况,从而没有可用于验证的 CSRF Tokens。spring-doc.cadn.net.cn

我们已经讨论过会话超时的通用解决方案。 本节将讨论与 Servlet 支持相关的 CSRF 超时的具体细节。spring-doc.cadn.net.cn

您可以将 CSRF Tokens的存储方式更改为使用 Cookie。 详细信息,请参阅 使用 CookieCsrfTokenRepository 部分。spring-doc.cadn.net.cn

如果Tokens过期,您可能希望通过指定自定义AccessDeniedHandler来自定义其处理方式。 自定义AccessDeniedHandler可以按您喜欢的方式处理InvalidCsrfTokenExceptionspring-doc.cadn.net.cn

多部分(文件上传)

我们已经讨论过,在防范CSRF攻击时保护多部分请求(文件上传)会引发一个先有鸡还是先有蛋的问题。 当JavaScript可用时,我们建议将CSRFTokens包含在HTTP请求头中,以绕开该问题。spring-doc.cadn.net.cn

如果 JavaScript 不可用,以下部分将讨论在 Servlet 应用程序中将 CSRF Tokens放置在请求体URL中的选项。spring-doc.cadn.net.cn

您可以在 Spring 参考文档的多部分解析器部分以及MultipartFilter javadoc中找到有关在 Spring 中使用多部分表单的更多信息。spring-doc.cadn.net.cn

将 CSRF Tokens放入请求体中

我们已经讨论过将 CSRF Tokens放在请求体中的权衡问题。 在本节中,我们将讨论如何配置 Spring Security 以从请求体中读取 CSRF Tokens。spring-doc.cadn.net.cn

为了从请求体中读取 CSRF Tokens,需将 MultipartFilter 配置在 Spring Security 过滤器之前。 将 MultipartFilter 放在 Spring Security 过滤器之前意味着调用 MultipartFilter 时不会进行授权检查,也就是说任何人都可以在您的服务器上放置临时文件。 然而,只有经过授权的用户才能提交由您的应用程序处理的文件。 通常情况下,这是推荐的做法,因为临时文件上传对大多数服务器的影响微乎其微。spring-doc.cadn.net.cn

配置 MultipartFilter
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 配置时 MultipartFilter 在 Spring Security 过滤器之前被指定,您可以在 <filter-mapping> 文件中将 MultipartFilterspringSecurityFilterChain 元素放置在 web.xml 之前。spring-doc.cadn.net.cn

在 URL 中包含 CSRF Tokens

如果无法接受允许未授权用户上传临时文件,替代方案是将 MultipartFilter 放置在 Spring Security 过滤器之后,并将 CSRF Tokens作为查询参数包含在表单的 action 属性中。 由于 CsrfToken 被公开为 HttpServletRequest 属性,名称为 _csrf,我们可以利用它创建一个包含 CSRF Tokens的 action。 以下示例展示了如何在 JSP 中实现这一点:spring-doc.cadn.net.cn

CSRF Tokens的实际应用
<form method="post"
	action="./upload?${_csrf.parameterName}=${_csrf.token}"
	enctype="multipart/form-data">

隐藏 HTTP 方法过滤器

我们已经讨论过将 CSRF Tokens放在请求体中的权衡问题。spring-doc.cadn.net.cn

在 Spring 的 Servlet 支持中,重写 HTTP 方法是通过使用 HiddenHttpMethodFilter 实现的。 您可以在参考文档的 HTTP 方法转换 部分找到更多信息。spring-doc.cadn.net.cn

进一步阅读

既然您已经了解了 CSRF 防护,接下来可以深入学习漏洞利用防护,包括安全响应头HTTP 防火墙,或者继续学习如何测试您的应用程序。spring-doc.cadn.net.cn