一次 403 的完整复盘:华为云 devdata 接口的 CSRF 握手是怎么做的
背景
做前端对接时遇到一个很典型的报错:接口文档写着 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);
}
...
};
三个关键信息:
- 令牌来源是响应头,不是响应体。
GET /api/get-ainfo返回体可能是空的,令牌在csrf响应头里。 - 只有命中白名单前缀的请求才会补令牌:
/rest/developer/fwdu、/rest/ecology/fwdc、/rest/ecology/fwdu。 - 令牌 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') 在同源/受信场景下才拿得到;跨域脚本读不到属于预期。
六、小结
这类「框架层统一注入令牌」的实现,排查顺序固定为三步:
- 区分
not login和no request_csrf,别在 Cookie 上纠缠 - 在页面脚本里找拦截器,定位令牌的来源 URL 与取出方式(头 or 体)
- 判断令牌的生命周期与作用域前缀,再决定封装位置
比起到处补 headers: { csrf },把握手收敛到一个 apiRequest 里,后续接口新增时零成本。
- 点赞
- 收藏
- 关注作者
评论(0)