Spring Security 跨站请求伪造保护
1. 前言
很多小伙伴在开发 Spring Security 项目时候,本地测试都没有问题,一放到生产环境后,就会遇到「Invalid CSRF Token」问题,这其实是 Spring Security 防止服务免受「跨站请求伪造」攻击攻击的防护行为。
跨站请求伪造(Cross Site Request Forgery),简写成「CSRF」或者「XSRF」,是一种挟持用户所用浏览器,执行非法操作的攻击方法,也就是说,攻击者利用「CSRF」漏洞伪造用户操作,可实现例如购物、注销等效果,还可以利用该漏洞配合产生其他多种攻击方式。
针对「CSRF」攻击最经济的解决方式是增加「Referer」头或者增加校验「Token」。
Spring Security 提供了针对「CSRF」的规范化防御手段,本节我们主要讨论如何使用 Spring Security 的内置功能防御「CSRF」攻击。
2. CSRF 攻击原理
我们用一个实例演示「CSRF」攻击的过程。
假设我们登陆了一个银行网站(bank.example.com),这个网站的作用是实现跨行转账的表单提交,通常情况下,我们会生成如下一个 Form 表单。
<form method="post" action="/transfer">
<!-- 汇款金额 -->
<input type="text" name="amount"/>
<!-- 汇款路由号 -->
<input type="text" name="routingNumber"/>
<!-- 汇款账户 -->
<input type="text" name="account"/>
<input type="submit" value="提交"/>
</form>
那我们发出的「post」请求格式可能如下:
POST /transfer HTTP/1.1
Host: bank.example.com
Cookie: JSESSIONID=randomid
Content-Type: application/x-www-form-urlencoded
amount=100.00&routingNumber=1234&account=9876
此时,如果我们未登出,并且访问了其他恶意网站,并且其他恶意网站同样包含了可提交的表单,表单形式如下:
<form method="post" action="https://bank.example.com/transfer">
<!-- 隐藏项不可见,转账金额,固定 100 元 -->
<input type="hidden" name="amount" value="100.00"/>
<!-- 隐藏项不可见,转账路由码 -->
<input type="hidden" name="routingNumber" value="evilsRoutingNumber"/>
<!-- 隐藏项不可见,转账账户 -->
<input type="hidden" name="account" value="evilsAccountNumber"/>
<!-- 可见 -->
<input type="submit" value="快来点我!"/>
</form>
当我们很好奇,点击了「快来点我」按钮时,我们会触发转账请求,并将钱汇款到一个未知账户里。在这个过程中,虽然恶意网站并不知道我们的「Cookies」值,但是由于未登出,我们和银行网站之间的 Cookies 还在,所以当我们再次发起请求时,该 Cookies 依然有效,这使得不知不觉被触发的转账请求同样有效。
除此之外,如果恶意网站使用 JS 脚本自动提交表单的话,用户可能没有任何被攻击的感觉。
3. CSRF 攻击的防御
我们虽然无法阻止恶意网站向目标网站发送 HTTP 请求,但是我们可以确保恶意网站无法生成目标网站所需的参数,所以就出现了如下两种常见的解决方案:
- 使用同步「Token」模式
- 在 Cookies 中指定网站同源的参数
这两种方式在 Spring Security 中都已支持。
3.1 CSRF 保护的前提
要实现 CSRF 保护,首先我们要确保安全方法是幂等的。安全方法包括「GET」,「HEAD」,「OPTIONS」,「TRACE」,幂等是指这些方法在反复发送后服务器状态不会改变。
3.2 同步「Token」模式
这种方法时防御「CSRF」攻击的最常用手段,它的原理是确保项目表网站发送的请求中,每次都包含一个被称为「CSRF Token」的随机参数。
每当请求提交到服务端后,服务端会对比请求中包含的「CSRF Token」和期望的是否匹配,如果不匹配,此请求作废。这里的关键是,「CSRF Token」不能由浏览器生成。
来看一下代码:
<form method="post" action="/transfer">
<!-- csrf 头 -->
<input type="hidden" name="_csrf" value="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
<!-- 汇款金额 -->
<input type="text" name="amount"/>
<!-- 汇款路由号 -->
<input type="text" name="routingNumber"/>
<!-- 汇款账户 -->
<input type="text" name="account"/>
<input type="submit" value="提交"/>
</form>
上述代码中增加了 _csrf
参数,且该值由服务器生成并埋在页面表单中,此时 HTTP 请求的内容如下:
POST /transfer HTTP/1.1
Host: bank.example.com
Cookie: JSESSIONID=randomid
Content-Type: application/x-www-form-urlencoded
amount=100.00&routingNumber=1234&account=9876&_csrf=4bfd1575-3ad1-4d21-96c7-4ef2d9f86721
由于浏览器同源策略,我们不用担心恶意网站可以得到 _csrf
的值,而且恶意网站无法伪造出于服务器想匹配的「CSRF Token」,这样就保护了用户信息的安全。
3.3 SameSite 参数
另一种避免「CSRF」攻击的方法是使用 SameSite
参数。这是一种新的方式,有赖于浏览器的支持。这种情况下服务端会指定一个 SameSite
参数来保障请求不会来自于非可信网站。
例如,包含 SameSite
参数的响应消息如下:
Set-Cookie: JSESSIONID=randomid; Domain=bank.example.com; Secure; HttpOnly; SameSite=Lax
其中 SameSite
属性支持的值包括:
- 严格模式(Strict)。仅 URL 相匹配的网站才支持发送 Cookie,任何其他网站都不会发生 Cookie。
- 宽松模式(Lax)。相比严格模式,宽松模式下支持部分非相同网站的请求中携带 Cookie,比如超链接方式跳转、预加载、GET请求。
采用这种方式后,来自恶意网站的请求无法加上 JSESSIONID
参数,由此避免了「CSRF」攻击。
4. Spring Security 配置方法
默认情况下,Spring Security 已开启「CSRF」保护,这里我们罗列一下其它常用配置。
4.1 自定义 Token 仓库
默认情况下,CSRF Token 存储在 HttpSession
中,使用 HttpSessionCsrfTokenRepository
对象维护。如果需要进行扩展,比如不仅要在 HTTP 请求中携带 Token,也需要在 JS 应用中应用 Token,那需要通过如下方式:
在配置类中构造并注入 CookieCsrfTokenRepository 对象。
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) {
http.csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()));
}
}
4.2 禁用 CSRF 保护
默认情况下,CSRF 保护功能已被开启,如果需要关闭,可通过如下方式配置:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) {
http.csrf(csrf -> csrf.disable());
}
}
4.3 携带 CSRF Token
为了在 HTTP 请求中携带 CSRF Token,我们必须要对 HTTP Request 做一些配置,因为它默认是不会携带 CSRF 相关参数的。默认情况下,Spring Security 中有 CsrfFilter
判断请求中是否有 _csrf
参数,通常请求来自于两种情况,Form 表单提交或者 Ajax。
4.3.1 Form 表单提交
使用 Form 表单提交代码时,我们需要在 Form 参数中增加一个隐藏项:_csrf
,例如:
<input type="hidden"
name="_csrf"
value="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
这里的 _csrf
有几种配置方式:
-
自动注入
Spring Security 通过扩展 Spring 的
RequestDataValueProcessor
类,实现了RequestDataValueProcessor
类,这意味着如果我们使用 Spring 标签库、Thymeleaf 模板插件、或者其它集成了RequestDataValueProcessor
对象的视图组件是,表单的非幂等请求(例如:POST
)都会自动携带 CSRF Token。 -
JSP 标签
针对 JSP 作为页面开发基础,我们可以直接使用 Spring 的表单标签库或者
CsrfInput
标签。也可以通过更加直接的方式,在使用HttpServletRequest
属性_csrf
,代码如下:<c:url var="logoutUrl" value="/logout"/> <form action="${logoutUrl}" method="post"> <input type="submit" value="登出" /> <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/> </form>
4.3.2 Ajax 和 JSON 请求
如果使用 Javascript 做为请求提交方式,我们没法直接使用 Http CSRF 参数,取而代之的是使用 Http 头的方式。这同样也有几种方法:
-
自动注入
Spring Security 可以自动将 CSRF Token 保存到 Cookie 中,一些客户端框架如 AngularJS 会自动从中得到 CSRF Token 并放置到请求头中。
-
Meta 标签
另一种方式是从 Cookie 中解压 Token 并使用 Meta 标签,如下:
<html> <head> <meta name="_csrf" content="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/> <meta name="_csrf_header" content="X-CSRF-TOKEN"/> <!-- ... --> </head>
当 Meta 标签中有 Token 信息时,我们就可以将 Meta 中的 CSRF Token 值用作请求参数了。以 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); }); });
5. 小结
本节我们讨论了 CSRF 的含义及在 Spring Security 中配置方法:
- CSRF 是一种常见的 B/S 攻击形式;
- CSRF 可以在浏览器上伪造用户的请求;
- CSRF 的防御思路是确保发送请求的来源是可信的;
- CSRF 的防御方法包含「同步 Token」和「设置 SameSite 参数」两种,其中「SameSite」参数方式需要浏览器的支持;
- Spring Security 默认已开启 CSRF 保护。
下节我们讨论 Spring Security 对 HTTP 请求常用的和安全相关的头部信息。