请求头验证实战:常见漏洞与安全加固方案

上官佳润 安全 阅读 2,265
赞 31 收藏
二维码
手机扫码查看
反馈

先看效果,再看代码

上周上线一个新功能,后端同事突然说:“前端加个请求头验证,不然接口不安全。”我一开始以为就是加个 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 做离线缓存时的头处理),后续会继续分享这类博客。希望这篇能帮你少踩几个坑。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论