CORS配置踩坑实录:从原理到实战的完整避坑指南

IT人春景 前端 阅读 2,396
赞 14 收藏
二维码
手机扫码查看
反馈

这次项目又碰到了CORS,真是老朋友了

最近接手了一个新的项目,前端Vue + 后端Node.js的架构。项目刚开始没多久就遇到了那个熟悉的老朋友——CORS跨域问题。其实按理说这问题我已经处理过无数次了,但每次换个项目环境还是会遇到一些意想不到的小状况。

CORS配置踩坑实录:从原理到实战的完整避坑指南

这次的场景稍微复杂一点,前端跑在localhost:3000,后端API在localhost:8080,还有一个第三方服务需要调用。刚开始以为就是常规配置,结果部署到测试环境后发现有些请求还是被拦截了,折腾了我一个下午。

基础配置其实挺简单的

Node.js那边我用的是express,CORS中间件配置基本就是下面这样:

const cors = require('cors');

// 基础配置
app.use(cors({
  origin: [
    'http://localhost:3000',
    'https://my-test-domain.com'
  ],
  credentials: true,
  optionsSuccessStatus: 200
}));

这个配置在本地开发环境跑得挺好,但问题就出在测试环境的域名配置上。开始我只配置了主域名,没考虑到可能还有子域名的请求,导致部分接口403报错。

最大的坑:预检请求和特殊头部处理

真正让我头疼的是预检请求(preflight)的问题。项目中有几个接口需要传自定义头部,比如Authorization和Content-Type application/json,这就触发了OPTIONS预检请求。

开始我配置得很粗糙,只允许了基本的头部:

app.use(cors({
  origin: ['http://localhost:3000', 'https://my-test-domain.com'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  methods: ['GET', 'POST', 'PUT', 'DELETE']
}));

但后来发现有些请求还是失败,查了半天才发现某些请求携带了额外的自定义头部。这时候就需要更详细的配置了:

const corsOptions = {
  origin: function (origin, callback) {
    // 本地开发环境允许所有localhost
    if (!origin || origin.includes('localhost')) {
      callback(null, true);
    } else {
      const allowedOrigins = [
        'https://my-test-domain.com',
        'https://sub.my-test-domain.com'
      ];
      if (allowedOrigins.indexOf(origin) !== -1) {
        callback(null, true);
      } else {
        callback(new Error('Not allowed by CORS'));
      }
    }
  },
  credentials: true,
  optionsSuccessStatus: 200,
  allowedHeaders: [
    'Origin', 
    'X-Requested-With', 
    'Content-Type', 
    'Accept', 
    'Authorization',
    'X-Custom-Header'
  ]
};

app.use(cors(corsOptions));

这里的动态origin处理花了我不少时间调试。开始想偷懒直接*通配符,但在生产环境这会有安全风险,而且配合credentials: true的时候会报错,浏览器不允许这样的组合。

前端也要注意的细节

后端配好了还不算完,前端这边也有需要注意的地方。特别是fetch请求时的credentials配置:

fetch('https://jztheme.com/api/user/profile', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': Bearer ${token}
  },
  credentials: 'include', // 这个很重要
  body: JSON.stringify(data)
})
.then(response => response.json())
.then(data => console.log(data));

开始我漏掉了credentials: ‘include’,导致cookie无法正常传递,用户登录状态就保持不住了。这个问题在Chrome控制台的Network面板里很容易发现,OPTIONS请求会返回401未授权。

还遇到过一个奇怪的问题,某些情况下即使CORS配置正确,IE浏览器还是会有兼容性问题。虽然现在IE用的人不多,但客户的要求还是要考虑,最后加了一些polyfill才搞定。

测试环境的特殊情况

部署到测试环境后发现了另一个问题。我们的测试环境用了一些特殊的代理配置,导致部分请求的实际来源和预期不一样。这里踩了个坑,花了不少时间排查。

为了方便调试,我加了一段临时的日志代码:

app.use((req, res, next) => {
  console.log('Request Origin:', req.headers.origin);
  console.log('Request Method:', req.method);
  console.log('Request Headers:', req.headers);
  next();
});

通过这些日志才发现,某些请求的origin头部是null,原来是浏览器安全策略的问题。针对这种情况我也做了特殊处理:

const corsOptions = {
  origin: function (origin, callback) {
    if (!origin) {
      // origin为null的情况,通常是同源或特殊请求
      callback(null, true);
    } else {
      // 原有的域名检查逻辑
      const allowedOrigins = [
        'http://localhost:3000',
        'https://my-test-domain.com'
      ];
      if (allowedOrigins.indexOf(origin) !== -1 || origin.includes('localhost')) {
        callback(null, true);
      } else {
        callback(new Error('Not allowed by CORS'));
      }
    }
  }
};

线上环境的安全考量

生产环境的CORS配置我比测试环境严格了很多。不再允许localhost,只精确匹配正式域名。另外还增加了一些安全相关的头部:

app.use((req, res, next) => {
  res.header('X-Content-Type-Options', 'nosniff');
  res.header('X-Frame-Options', 'DENY');
  res.header('X-XSS-Protection', '1; mode=block');
  next();
});

这部分配置虽然不是CORS的核心,但对于整体安全性还是很重要的。

性能方面的考虑

CORS预检请求确实会对性能有一定影响,特别是对于频繁的API调用。大部分浏览器会缓存预检结果,默认是5分钟。如果接口特别频繁,可以考虑增加缓存时间:

app.use((req, res, next) => {
  res.header('Access-Control-Max-Age', '86400'); // 缓存1天
  next();
});

但要注意,修改了CORS配置后,客户端的缓存可能还会沿用旧配置,需要清缓存或者等待过期。

调试工具推荐

这次排查过程中用到的一些工具也分享一下。Chrome DevTools的Network面板是最基本的,看OPTIONS请求的状态码和响应头部。Postman在测试不同origin的请求时也很有用,可以模拟各种HTTP头部。

还用了个小技巧,在服务器端加了详细的错误日志,记录被拒绝的CORS请求来源,方便定位问题:

const corsOptions = {
  origin: function (origin, callback) {
    const allowedOrigins = [
      'https://my-test-domain.com',
      'http://localhost:3000'
    ];
    
    if (allowedOrigins.indexOf(origin) !== -1) {
      callback(null, true);
    } else {
      console.warn('CORS blocked:', origin, 'at', new Date().toISOString());
      callback(new Error('Not allowed by CORS'));
    }
  }
};

回过头看,有些地方还可以优化

总的来说这次CORS配置算是搞定了,但回过头看还是有几个可以改进的地方。首先是配置管理,现在这些域名都是硬编码的,如果能通过环境变量来配置会更灵活。其次是监控,现在出了问题主要靠用户反馈才知道,应该加个更主动的监控机制。

还有一个小问题至今没完全解决,就是移动端Safari偶尔会有一些兼容性问题,不过影响范围不大,暂时先搁置了。后续如果遇到更多设备的问题,可能要考虑更全面的测试覆盖。

这个项目的CORS配置算是比较完整的方案了,虽然过程中踩了不少坑,但至少以后遇到类似问题心里有底了。现在的配置已经稳定运行了一周多,没有再出现跨域相关的问题。

最后总结

以上是我踩坑后的总结,希望对你有帮助。CORS看起来是个简单的问题,但实际项目中还是会遇到各种意想不到的情况,特别是涉及到不同环境部署的时候。记住几个要点:准确的域名配置、适当的头部设置、安全的策略考虑,以及充分的测试覆盖。

这个技巧的拓展用法还有很多,后续会继续分享这类博客。有更优的实现方式欢迎评论区交流。

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

暂无评论