请求头验证实战:常见漏洞与安全加固方案
先看效果,再看代码
上周上线一个新功能,后端同事突然说:“前端加个请求头验证,不然接口不安全。”我一开始以为就是加个 Authorization 就完事了,结果折腾了半天才发现,事情没那么简单。今天就来聊聊我在实际项目里怎么处理请求头验证的,附带完整代码和踩过的坑。
核心其实就两件事:一是发请求时带上正确的头,二是拦截异常响应做统一处理。下面这个封装是我现在项目里用的,亲测有效:
// utils/request.js
const API_BASE = 'https://jztheme.com/api';
function createRequest(config) {
const defaultConfig = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
// 这里是关键,从 localStorage 或其他地方拿 token
'Authorization': Bearer ${localStorage.getItem('auth_token') || ''},
},
credentials: 'include', // 如果需要带 cookie,比如 CSRF 防护
};
const mergedConfig = { ...defaultConfig, ...config };
return fetch(${API_BASE}${config.url}, mergedConfig)
.then(res => {
// 先检查状态码
if (!res.ok) {
throw new Error(HTTP ${res.status});
}
return res.json();
})
.catch(err => {
console.error('请求失败:', err);
// 这里可以统一处理 401、403 等
if (err.message.includes('401')) {
// token 过期,跳登录
localStorage.removeItem('auth_token');
window.location.href = '/login';
}
throw err;
});
}
// 使用示例
createRequest({ url: '/user/profile' })
.then(data => console.log(data))
.catch(err => console.error('业务错误:', err));
这段代码看着简单,但里面埋了几个雷,后面会细说。
这个场景最好用
请求头验证最常出现在三种场景:
- 身份认证:比如
Authorization: Bearer xxx,这是最常见的 - 防爬虫/防刷:比如要求必须带
X-Requested-With: XMLHttpRequest,或者自定义头如X-Client-Version - CSRF 防护配合:虽然主流是用 SameSite Cookie,但有些老系统还是会校验
X-CSRF-Token
我最近做的一个后台系统,就同时用了前两种。后端要求所有 AJAX 请求必须带 X-Requested-With,否则直接返回 403。一开始我忘了加,本地测试没问题(因为本地没开这个校验),一上测试环境就全挂了,查了半小时才定位到是头的问题。
所以建议:**只要后端有头校验逻辑,前端封装层一定要统一加,别每个接口单独写**。不然哪天漏了一个,半夜被叫起来修 bug 的就是你。
踩坑提醒:这三点一定注意
第一,跨域时的预检请求(Preflight)。如果你加了自定义头(比如 X-Auth-Token),浏览器会先发一个 OPTIONS 请求。如果后端没正确处理 OPTIONS,你的请求根本发不出去。我之前就遇到过,后端只允许 Authorization,但我在本地调试时用了 X-Debug-Token,结果 OPTIONS 被 405 了。解决办法很简单:要么让后端放开 OPTIONS,要么统一用标准头(比如就用 Authorization)。
第二,token 刷新的竞态问题。假设你有两个并行请求,都因为 token 过期返回 401。这时候如果两个请求都去刷新 token,就会导致重复刷新,甚至覆盖掉新的 token。我的做法是加一个“刷新锁”:
let isRefreshing = false;
let refreshSubscribers = [];
function onTokenRefreshed(newToken) {
refreshSubscribers.forEach(callback => callback(newToken));
refreshSubscribers = [];
}
function addSubscriber(callback) {
refreshSubscribers.push(callback);
}
// 在 fetch 拦截器里
if (res.status === 401) {
if (!isRefreshing) {
isRefreshing = true;
refreshToken().then(newToken => {
localStorage.setItem('auth_token', newToken);
isRefreshing = false;
onTokenRefreshed(newToken);
});
}
// 返回一个 pending promise,等新 token
return new Promise((resolve) => {
addSubscriber(token => {
// 用新 token 重试原请求
resolve(createRequest(originalConfig));
});
});
}
这段代码有点啰嗦,但能解决问题。如果你用 axios,它有现成的 interceptors 可以更优雅地处理,但 fetch 原生就得自己搞。
第三,不要在头里放敏感信息明文。比如有人喜欢把用户 ID 放 X-User-Id,这其实很危险。因为请求头可能被代理、日志记录,甚至被 XSS 读取(虽然现代浏览器限制了 JS 读某些头,但别冒险)。正确的做法是:所有敏感信息都走 token,由后端解析 JWT 或查 session。
高级技巧:动态头和环境隔离
有时候不同环境需要不同的头。比如开发环境要加 X-Debug: true,生产环境不能有。我见过有人用 if-else 写死,但更好的方式是用配置驱动:
const ENV_HEADERS = {
development: {
'X-Debug': 'true',
'X-Mock-User': 'admin',
},
production: {},
};
const currentEnv = process.env.NODE_ENV || 'development';
const extraHeaders = ENV_HEADERS[currentEnv] || {};
// 合并到 defaultConfig.headers 里
另外,有些接口不需要认证(比如登录、注册),记得在调用时覆盖掉默认头:
createRequest({
url: '/auth/login',
headers: {
'Authorization': '', // 清空默认的 auth 头
},
method: 'POST',
body: JSON.stringify({ email, password }),
});
或者更优雅点,在封装层判断 URL 白名单:
const PUBLIC_PATHS = ['/auth/login', '/auth/register'];
function shouldSkipAuth(url) {
return PUBLIC_PATHS.some(path => url.startsWith(path));
}
// 在 createRequest 里
if (!shouldSkipAuth(config.url)) {
// 加 auth 头
}
最后说两句
请求头验证看起来是个小功能,但一旦出问题,排查起来特别费劲——因为错误往往不是“代码报错”,而是“接口静默失败”。所以我现在养成了习惯:**所有新接口,先用 curl 或 Postman 手动测一遍头,确认后端逻辑没问题,再写前端**。
以上是我个人对请求头验证的完整实战总结,有更优的实现方式欢迎评论区交流。这个技术的拓展用法还有很多(比如结合 Service Worker 做离线缓存时的头处理),后续会继续分享这类博客。希望这篇能帮你少踩几个坑。

暂无评论