CSRF 是 Cross-site request forgery 的全称,中文翻译为“跨站请求伪造”,是互联网上常见的一种攻击方式。
CSRF 原理
假设用户在正常使用并登录某个网站,攻击者诱导用户进入第三方网站,在第三方网站向被攻击网站发送跨站请求。
由于用户在被攻击网站已经登录,攻击者的请求会被认为是已登录的合法请求,从而达到冒充用户进行某些操作的目的。
攻击范围
这种攻击方式只针对 http session 登录的网站有效,因为登录信息保存在 cookie 中,而 cookie 信息会自动作为请求的一部分发送到服务器。
如果使用 jwt 鉴权,登录信息使用 header 传递,不依赖 cookie,不会自动发送到服务器,所以不存在 CSRF 漏洞。
CSRF 防范
为了防范 CSRF,需要在提交数据时增加校验,要求提交一个第三方网站无法知道的数据,这就是 CSRF Token。完整步骤如下:
服务器生成一个随机的
CSRF Token
用户打开表单提交页面时,将
CSRF Token
写入页面当用户提交数据时,附带
CSRF Token
数据服务器检验请求中的
CSRF Token
是否正确
Spring Security 中默认开启了这个功能,会对所有 POST
请求都进行校验。如果请求中没有附带 CSRF Token
数据,将禁止提交,并给出以下提示:
Invalid CSRF Token 'null' was found on the request parameter '_csrf' or header 'X-CSRF-TOKEN'.
所有操作数据的请求全部使用 POST
方法提交,不能使用 GET
方法提交数据。根据 HTTP 规范 get 请求只获取数据,不提交和修改数据,所以不对 get 请求进行 CSRF 校验。
获取 Token
要将 Token 放到请求中,就必须先在服务器中生成,然后在页面中获取。Spring Security 会自动生成好 Token,并将它放到 ModelMap 中,名称为 _csrf
,类型为 org.springframework.security.web.csrf.CsrfToken
,页面中可以直接获取。
CsrfToken 包含以下属性:
headerName: 请求 header 中的名称
parameterName: 请求表单中的名称
token: Token 值
例如,要获取 Token 值,可直接在页面中写 ${_csrf.token}
。
CSRF 校验方式
如何将 Token 放到请求中呢?Spring Security 支持有两种方式,使用其中的一种即可:
将 Token 放到请求表单里,作为表单的一个字段,这个字段名可通过
${_csrf.parameterName}
获取,默认为_csrf
。将 Token 放到请求 header 里,header 名可通过
${_csrf.headerName}
获取,默认为X-CSRF-TOKEN
。
最佳实践
通常会先将 headerName 和 token 放到页面的 meta 中:
<meta name="_csrf" content="${_csrf.token}"/>
<meta name="_csrf_header" content="${_csrf.headerName}"/>
注意:所有需要提交数据的页面都需要此 meta 信息,否则无法获取 token 信息。
提交请求时,获取 meta 中的值,放到 header 中:
// 使用 axios 的拦截器,统一加入 csrf 数据
axios.interceptors.request.use(function(config){
const method = config.method.toLowerCase();
// get 请求不需要 csrf 校验
if (method === 'post' || method === 'put' || method === 'delete') {
const header = $('meta[name="_csrf_header"]').attr('content');
const token = $('meta[name="_csrf"]').attr('content');
config.headers = {...config.headers, [header]: token};
}
return config;
})
在某些不方便操作 header 的场合,也可以直接在表单加入 token 信息:
<form action="/abc" method="POST">
...
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
</form>
静态化页面处理
如果生成了静态化页面,服务器重启后,会重新生成 CSRF Token
,而静态页面中的 token 值还是以前的数据。
这时需要在提交数据前,动态获取 token 值,写入页面 meta 标签。UJCMS 提供了动态获取 token 的接口:/api/env/csrf
或 /frontend/env/csrf
。
// 动态获取 csrf token,并写入页面的 meta 标签
function fetchCsrf() {
return axios.get('${api}/env/csrf').then(function (response) {
const data = response.data;
if (data && data.headerName && data.token) {
$('meta[name="_csrf_header"]').attr('content', data.headerName);
$('meta[name="_csrf"]').attr('content', data.token);
} else {
throw new Error('CSRF no response data.');
}
});
}
// 提交数据前,先动态获取 CSRF Token,然后再执行提交请求
fetchCsrf().then(function() {
request.post('...');
});
错误排查
如果代码中加入了 CSRF Token 的处理,却依然出现 CSRF 报错的问题,可以通过浏览器的调试功能,查看请求的 header 或表单字段是否包含 CSRF Token 数据。