CSRF 问题

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 数据。

2025-04-25 12:57
Last Updated: 2025-04-25
CONTENTS
0791-85271700
QQ咨询:1779755751
QQ交流群:626599871
微信咨询
微信扫码咨询
微信交流群
微信交流群
Powered by UJCMS © 2010-2025 All Rights Reserved
QQ咨询
电话
微信
微信扫码咨询