一次 403 的完整复盘:华为云 devdata 接口的 CSRF 握手是怎么做的

举报
doraemon001 发表于 2026/11/08 18:45:20 2026/11/08
【摘要】 背景做前端对接时遇到一个很典型的报错:接口文档写着 GET,参数也没错,Cookie 也带上了,但服务端始终返回:HTTP/2 403no request_csrf found换成「浏览器里明明能打开」的接口也一样。这篇文章把这层 CSRF 握手的机制拆开讲清楚,顺带给出一个可以直接复用的通用封装。一、先确认不是登录问题第一反应通常是会话失效。但同一时刻:页面右上角正常显示账号名GET /g...

背景

做前端对接时遇到一个很典型的报错:接口文档写着 GET,参数也没错,Cookie 也带上了,但服务端始终返回:

HTTP/2 403
no request_csrf found

换成「浏览器里明明能打开」的接口也一样。这篇文章把这层 CSRF 握手的机制拆开讲清楚,顺带给出一个可以直接复用的通用封装。

一、先确认不是登录问题

第一反应通常是会话失效。但同一时刻:

  • 页面右上角正常显示账号名
  • GET /growth/bonus-beans 在页面里能拿到 {"beans":400}
  • 用 curl 带同一份 Cookie 却是 no request_csrf found

no request_csrf 和 not login 是两个不同的分支,前者说明身份校验已经通过,缺的是 CSRF 令牌。这一步能省掉大量在 Cookie 上的无用排查。

二、令牌从哪来

翻开站点加载的 developer-pep2-template.js,会看到一段被包裹在 jQuery 里的 XHR 拦截逻辑:

var _csrf,
    csrfCheckServices = ["/rest/developer/fwdu", "/rest/ecology/fwdc", "/rest/ecology/fwdu"],
    _csrfurl = devdataDomain + "/api/get-ainfo",
    originOpen = XHR.prototype.open,
    originSend = XHR.prototype.send,
    originSetHeader = XHR.prototype.setRequestHeader;

XHR.prototype.setRequestHeader = function (header, value) {
  this._headers = this._headers || {};
  this._headers[header] = value;
  originSetHeader.apply(this, arguments);
};

XHR.prototype.send = function (body) {
  if (csrfCheckServices.find(s => this._url.indexOf(s) !== -1)
      && (!this._headers || !this._headers.csrf)) {

    if (!_csrf) {
      var csrfXhr = new XMLHttpRequest;
      csrfXhr.open("GET", _csrfurl, false);          // 注意:同步
      csrfXhr.withCredentials = true;
      csrfXhr.onreadystatechange = function () {
        if (csrfXhr.readyState === 4 && csrfXhr.status === 200) {
          _csrf = csrfXhr.getResponseHeader("csrf") || void 0;   // 从响应头取
        }
      };
      csrfXhr.send();
      var refreCsrfTimer = setInterval(arguments.callee, 6e5);   // 10 分钟刷新
    }
    this.setRequestHeader("csrf", _csrf);
  }
  ...
};

三个关键信息:

  1. 令牌来源是响应头,不是响应体。GET /api/get-ainfo 返回体可能是空的,令牌在 csrf 响应头里。
  2. 只有命中白名单前缀的请求才会补令牌:/rest/developer/fwdu、/rest/ecology/fwdc、/rest/ecology/fwdu。
  3. 令牌 10 分钟刷新一次,缓存在闭包变量里,同一页面生命周期内复用。

三、为什么 curl 一定失败

/api/get-ainfo 用 curl 加磁盘上的 Cookie 请求,会得到:

HTTP/2 302
location: /common/404.html

定位到原因是会话 Cookie 的存储属性:

SID          persistent=0   host=.huaweicloud.com
SessionID    persistent=0   host=.huaweicloud.com
csrf-eco     persistent=0   host=.huaweicloud.com
cbc-sid      persistent=1   expires=2027-11-07

persistent=0 表示会话 Cookie,只活在浏览器进程内存里,磁盘上的那份是旧值。所以「从 Cookie 数据库导出再直连」这条路,拿到的是过期凭证,get-ainfo 直接 302,令牌自然取不到。

结论:必须在真实浏览器上下文里完成握手,而不是靠离线 Cookie。

四、可复用的握手封装

把上面的逻辑抽成一个同步函数,后面所有请求都走它:

function apiRequest(method, url, payload) {
  // 1) 握手:取 CSRF
  var csrf = window.__csrf;
  if (!csrf) {
    var c = new XMLHttpRequest();
    c.open('GET', 'https://devdata.huaweicloud.com/api/get-ainfo', false);
    c.withCredentials = true;
    c.send();
    csrf = window.__csrf = c.getResponseHeader('csrf');
  }

  // 2) 业务请求
  var r = new XMLHttpRequest();
  r.open(method, url, false);
  r.withCredentials = true;
  r.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
  r.setRequestHeader('csrf', csrf);
  r.send(payload ? JSON.stringify(payload) : null);

  return { status: r.status, body: r.responseText };
}

调用示例:

apiRequest('GET', BASE + '/bonus-beans');
// { status: 200, body: '{"beans":400}' }

apiRequest('POST', MISSION_URL, {
  page_no: 1, page_size: 100, category_key: 'DAILY_MISSIONS'
});
// { status: 200, body: '{"total":14,"QueryMissionDtoList":[...]}' }

五、几个容易踩的坑

现象 原因 处理
no request_csrf found 缺 csrf 请求头 先 GET /api/get-ainfo 取响应头
令牌取到是 null 请求 302 到 404 页 检查会话是否有效,别用离线 Cookie
令牌用一会儿就失效 服务端 10 分钟轮换 缓存设 TTL,或复用页面内已有闭包
响应头读不到 跨域未暴露该头 服务端 Access-Control-Expose-Headers 里要有 csrf

顺带一提,这个接口的 access-control-expose-headers 正好写明了可读的响应头:

access-control-expose-headers: wise_traceid,csrf,X-Language,x-transaction-id,workstation-id

csrf 在列,所以 getResponseHeader('csrf') 在同源/受信场景下才拿得到;跨域脚本读不到属于预期。

六、小结

这类「框架层统一注入令牌」的实现,排查顺序固定为三步:

  1. 区分 not login 和 no request_csrf,别在 Cookie 上纠缠
  2. 在页面脚本里找拦截器,定位令牌的来源 URL 与取出方式(头 or 体)
  3. 判断令牌的生命周期与作用域前缀,再决定封装位置

比起到处补 headers: { csrf },把握手收敛到一个 apiRequest 里,后续接口新增时零成本。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。