阅读视图

发现新文章,点击刷新页面。

Koa2 跨域实战:`withCredentials`场景下响应头配置全解析

第一章 跨域问题本质:同源策略与凭证携带限制

1.1 同源策略的核心规则

同源策略(Same-Origin Policy)是浏览器最核心的安全机制之一,其定义为:协议、域名、端口三者完全一致才视为同源。当浏览器发起跨域请求(如前端域名https://web.example.com访问后端https://api.example.com)时,会受到以下限制:

  • 默认禁止读取响应内容(如 JSON 数据)
  • 默认禁止携带凭证(如 Cookie、HTTP 认证头)
  • 需通过 CORS 响应头显式授权

1.2 withCredentials的特殊限制

当前端通过以下方式显式要求携带凭证时:

// Fetch API
fetch('https://api.example.com/data', { credentials: 'include' });

// XMLHttpRequest
xhr.withCredentials = true;

浏览器会对服务端响应头施加更严格的验证规则:

  1. Access-Control-Allow-Origin禁止使用*
    必须返回具体的源(如https://web.example.com),否则浏览器将拦截响应,报错:
    The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'

  2. 必须返回Access-Control-Allow-Credentials: true
    显式告知浏览器允许携带凭证,否则凭证不会被发送。

  3. 预检请求(OPTIONS)的特殊要求
    非简单请求(如 PUT/DELETE 方法、自定义请求头)需先发送 OPTIONS 请求验证权限,服务端必须正确响应以下头:

    Access-Control-Allow-Methods: POST, GET, OPTIONS
    Access-Control-Allow-Headers: Content-Type, Authorization
    

第二章 Koa2 核心解决方案:动态源与中间件配置

2.1 手动处理 CORS 响应头(无中间件)

通过 Koa 中间件手动解析请求源并动态设置响应头,适合轻量级项目或需要高度定制化的场景。

2.1.1 基础实现:白名单校验

const Koa = require('koa');
const app = new Koa();

// 允许的源白名单(生产环境建议从环境变量读取)
const ALLOWED_ORIGINS = new Set([
  'https://web.example.com',
  'https://admin.example.com:8080' // 包含端口的完整源
]);

app.use(async (ctx, next) => {
  const origin = ctx.get('Origin'); // 获取前端请求的源
  
  // 校验源是否合法
  if (ALLOWED_ORIGINS.has(origin)) {
    ctx.set('Access-Control-Allow-Origin', origin);
    ctx.set('Access-Control-Allow-Credentials', 'true'); // 必须设置为true
    ctx.set('Access-Control-Expose-Headers', 'Authorization, X-Total-Count'); // 允许前端读取的自定义头
  } else {
    // 不允许的源返回403或不设置Access-Control-Allow-Origin(浏览器自动拒绝)
    if (ctx.method === 'OPTIONS') {
      ctx.status = 403;
      return;
    }
  }

  // 处理预检请求
  if (ctx.method === 'OPTIONS') {
    ctx.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
    ctx.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    ctx.set('Access-Control-Max-Age', '86400'); // 预检结果缓存24小时
    ctx.status = 204; // 成功响应预检请求
    return;
  }

  await next();
});

// 示例路由
app.use(async ctx => {
  if (ctx.path === '/api/data') {
    ctx.body = { data: 'Authenticated response' };
    ctx.set('Authorization', 'Bearer xxx'); // 配合Access-Control-Expose-Headers使用
  }
});

app.listen(3000);

2.1.2 进阶场景:通配符匹配(需谨慎)

允许同一域名下的所有子域名(如*.example.com),可通过正则表达式实现:

const ALLOWED_ORIGIN_REGEX = /^https?://(?:\w+.)?example.com$/;

app.use(async (ctx, next) => {
  const origin = ctx.get('Origin');
  if (origin && ALLOWED_ORIGIN_REGEX.test(origin)) {
    ctx.set('Access-Control-Allow-Origin', origin);
    ctx.set('Access-Control-Allow-Credentials', 'true');
  }
  // ...处理预检请求
});

警告:通配符匹配可能引入安全风险,仅建议在可控环境(如同一域名下的子系统)使用。

2.2 使用koa-cors中间件简化配置

koa-cors是 Koa 官方推荐的 CORS 中间件,支持动态源、凭证配置及预检请求处理,代码量可减少 50% 以上。

2.2.1 基础配置

const Koa = require('koa');
const cors = require('koa-cors');
const app = new Koa();

app.use(cors({
  origin: function(ctx) {
    // 动态返回允许的源,优先匹配白名单
    const allowed = ['https://web.example.com', 'https://admin.example.com'];
    const origin = ctx.get('Origin');
    if (allowed.includes(origin)) {
      return origin;
    }
    // 开发环境允许本地调试(需限制在开发阶段)
    if (process.env.NODE_ENV === 'development' && origin.includes('localhost')) {
      return origin;
    }
    return false; // 禁止的源返回false,浏览器会忽略该头
  },
  credentials: true, // 允许携带凭证
  maxAge: 86400, // 预检缓存时间
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  headers: ['Content-Type', 'Authorization', 'X-Custom-Header']
}));

app.listen(3000);

2.2.2 配置简写:环境变量驱动

app.use(cors({
  origin: ctx => ctx.get('Origin') || '*', // 生产环境需移除*,此处仅为示例
  credentials: true,
  // 从环境变量读取允许的方法和头
  methods: process.env.ALLOWED_METHODS.split(','),
  headers: process.env.ALLOWED_HEADERS.split(',')
}));

第三章 生产环境最佳实践:安全与性能优化

3.1 安全加固策略

3.1.1 严格限制允许的源

  • 禁止使用* :无论是否携带凭证,生产环境必须使用白名单

  • 白名单来源

    • 前端正式环境域名(如https://example.com
    • 移动端 API 域名(如api.example.com
    • 第三方合作平台域名(需提前审核)

3.1.2 防范 CSRF 攻击

  • 启用SameSite Cookie 属性:

    ctx.cookies.set('sessionId', 'xxx', {
      sameSite: 'strict', // 严格模式,禁止跨站发送Cookie
      secure: true, // 仅通过HTTPS传输
      httpOnly: true // 禁止JS读取Cookie
    });
    
  • 验证Referer头(配合 CORS 白名单):

    app.use(async (ctx, next) => {
      const referer = ctx.get('Referer');
      if (referer && !ALLOWED_ORIGINS.has(new URL(referer).origin)) {
        ctx.throw(403, 'Invalid referer');
      }
      await next();
    });
    

3.1.3 限制请求方法与头

  • 仅允许必要的 HTTP 方法(如GET/POST),禁止危险方法(如PUT/DELETE直接暴露在外网)

  • 严格控制允许的请求头,避免开放*

    // 错误示例(开放所有头,存在安全风险)
    ctx.set('Access-Control-Allow-Headers', '*');
    
    // 正确示例(仅允许必要头)
    ctx.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    

3.2 性能优化

3.2.1 预检请求缓存

通过Access-Control-Max-Age头设置预检结果缓存时间(单位:秒),避免重复 OPTIONS 请求:

ctx.set('Access-Control-Max-Age', '86400'); // 缓存24小时

3.2.2 静态资源与 API 分离

  • 静态资源(JS/CSS/ 图片)通过 CDN 分发,设置Access-Control-Allow-Origin: *(无需凭证)
  • API 接口单独部署,严格限制允许的源并启用凭证校验

第四章 典型场景解决方案

4.1 前后端分离开发(本地调试)

前端环境:

  • 开发服务器地址:http://localhost:8080
  • 后端 API 地址:http://localhost:3000

Koa2 配置:

const ALLOWED_ORIGINS = new Set([
  'http://localhost:8080', // 前端开发地址
  'http://127.0.0.1:8080' // 兼容不同本地IP访问
]);

// 开发环境允许动态添加源(仅用于调试)
if (process.env.NODE_ENV === 'development') {
  ALLOWED_ORIGINS.add(ctx.get('Origin')); // 临时信任首次请求的源
}

前端请求:

fetch('http://localhost:3000/api/data', {
  credentials: 'include', // 携带本地Cookie(如登录态)
});

4.2 多前端应用共享 API(如 Web+App + 小程序)

允许的源列表:

const ALLOWED_ORIGINS = new Set([
  'https://web.example.com', // Web端
  'https://app.example.com', // 移动端App
  'https://miniprogram.example.com' // 小程序
]);

差异化配置:

app.use(async (ctx, next) => {
  const origin = ctx.get('Origin');
  if (ALLOWED_ORIGINS.has(origin)) {
    ctx.set('Access-Control-Allow-Origin', origin);
    
    // 根据不同源返回不同响应头
    if (origin.includes('miniprogram')) {
      ctx.set('Access-Control-Allow-Headers', 'Content-Type'); // 小程序仅需基础头
    } else {
      ctx.set('Access-Control-Allow-Headers', 'Content-Type, Authorization'); // Web端需要认证头
    }
  }
  await next();
});

4.3 第三方应用授权访问(需严格审核)

场景:

允许经过认证的第三方应用(如partner.example.com)访问 API,但需限制 IP 来源。

实现方案:

app.use(async (ctx, next) => {
  const origin = ctx.get('Origin');
  const clientIp = ctx.ip; // 获取客户端IP
  
  // 校验源与IP绑定关系
  if (origin === 'https://partner.example.com' && clientIp === '192.168.1.100') {
    ctx.set('Access-Control-Allow-Origin', origin);
    ctx.set('Access-Control-Allow-Credentials', 'true');
  }
  await next();
});

第五章 常见问题与排错指南

5.1 浏览器报错:Access-Control-Allow-Origin缺失

可能原因:

  1. 服务端未返回Access-Control-Allow-Origin
  2. 源校验失败,服务端未设置任何允许的源

解决方案:

  • 通过浏览器开发者工具(F12)查看响应头,确认是否存在该头
  • 检查白名单是否包含当前请求的源(包括端口)

5.2 凭证未携带(Cookie 未发送到服务端)

可能原因:

  1. Access-Control-Allow-Credentials未设置为true
  2. 服务端返回的Access-Control-Allow-Origin与请求的源不一致(如缺少端口)

解决方案:

// 确保同时设置两者
ctx.set('Access-Control-Allow-Origin', 'https://web.example.com:8080');
ctx.set('Access-Control-Allow-Credentials', 'true');

5.3 预检请求(OPTIONS)失败

可能原因:

  1. 未处理 OPTIONS 请求,返回 404 或其他错误状态码
  2. Access-Control-Allow-Methods未包含实际请求的方法(如 POST 请求但仅允许 GET)

解决方案:

// 手动处理OPTIONS请求
app.use(async (ctx, next) => {
  if (ctx.method === 'OPTIONS') {
    ctx.status = 204;
    ctx.set('Access-Control-Allow-Methods', 'GET, POST'); // 包含实际使用的方法
    return;
  }
  await next();
});

第六章 完整项目示例:Koa2 + Vue3 跨域实战

6.1 项目结构

koa2-api/
├── src/
│   ├── app.js               # Koa主文件
│   ├── middleware/
│   │   └── cors.js          # CORS中间件
│   └── routes/
│       └── api.js           # API路由
├── package.json
└── config/
    └── cors.js              # CORS配置文件

6.2 配置文件(config/cors.js

module.exports = {
  allowedOrigins: new Set([
    'https://vue.example.com', // Vue生产环境
    'http://localhost:8080'    // Vue开发环境
  ]),
  credentials: true,
  maxAge: 86400
};

6.3 CORS 中间件(middleware/cors.js

const { allowedOrigins, credentials, maxAge } = require('../config/cors');

module.exports = async (ctx, next) => {
  const origin = ctx.get('Origin');
  if (allowedOrigins.has(origin)) {
    ctx.set('Access-Control-Allow-Origin', origin);
    ctx.set('Access-Control-Allow-Credentials', String(credentials));
    ctx.set('Access-Control-Max-Age', String(maxAge));
  }

  if (ctx.method === 'OPTIONS') {
    ctx.set('Access-Control-Allow-Methods', 'GET, POST');
    ctx.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    ctx.status = 204;
    return;
  }

  await next();
};

6.4 主文件(src/app.js

const Koa = require('koa');
const cors = require('./middleware/cors');
const apiRouter = require('./routes/api');

const app = new Koa();

app.use(cors);
app.use(apiRouter.routes());

app.listen(3000, () => {
  console.log('Koa2 API server running on port 3000');
});

6.5 前端(Vue3)请求示例

<template>
  <button @click="fetchData">获取数据</button>
</template>

<script setup>
import { ref } from 'vue';
import axios from 'axios';

const data = ref('');

const fetchData = async () => {
  try {
    const response = await axios.get('https://api.example.com/api/data', {
      withCredentials: true, // 携带Cookie
      headers: {
        Authorization: `Bearer ${localStorage.getItem('token')}`
      }
    });
    data.value = response.data;
  } catch (error) {
    console.error('跨域请求失败:', error);
  }
};
</script>

第七章 未来趋势:跨域方案的演进

7.1 HTTP2 与跨域优化

HTTP2 的多路复用特性可减少跨域请求的性能损耗,配合Alt-Svc头实现域名切换:

Alt-Svc: h2=":443"; ma=2592000

7.2 边缘计算与 CORS 卸载

通过 CDN 边缘节点处理 CORS 头,减轻源站压力:

# 示例Nginx配置
location /api/ {
  proxy_pass http://koa-server;
  add_header Access-Control-Allow-Origin $http_origin;
  add_header Access-Control-Allow-Credentials true;
}

7.3 同站策略(SameSite)替代跨域

通过调整 Cookie 的SameSite属性,将跨域请求转为同站请求(需前后端共享域名):

// 后端设置
ctx.cookies.set('sessionId', 'xxx', {
  sameSite: 'lax', // 允许跨站GET请求携带Cookie
  domain: '.example.com' // 共享同一域名
});

结语

withCredentials场景下,Koa2 通过动态源校验、中间件封装及安全策略配置,可有效解决跨域凭证携带问题。核心原则是:严格限制允许的源,避免滥用通配符,始终遵循最小权限原则。通过结合环境变量、配置文件及安全加固措施,既能满足开发效率需求,又能保障生产环境的安全性。未来随着浏览器策略的升级,建议持续关注 CORS 规范的演进,采用更高效的同站策略或边缘计算方案优化跨域体验。

❌