跨域解决方案

What — 是什么

浏览器的同源策略(Same-Origin Policy)限制不同源的 JS 读取资源,跨域解决方案用于合法绕过这一限制实现前后端通信。

同源策略

同源定义: 协议(Protocol)+ 域名(Host)+ 端口(Port)完全一致才算同源,任一不同即为跨域。

URL AURL B是否同源原因
https://example.com/ahttps://example.com/b同源协议/域名/端口一致
https://example.comhttp://example.com跨域协议不同(https vs http)
https://example.comhttps://www.example.com跨域域名不同(子域也算不同源)
https://example.comhttps://example.com:8080跨域端口不同
https://example.comhttps://192.168.1.1跨域域名与 IP 不同

同源策略的完整限制范围:

限制维度受限行为说明
DOM 访问跨源 iframe 的 JS 无法读写对方 DOMdocument.getElementById 等操作被拒绝
Cookie / Storage无法读取跨源的 Cookie、LocalStorage、IndexedDB即使是同域不同端口也不行
XMLHttpRequest / Fetch跨源网络请求的响应被浏览器拦截请求已发出,只是 JS 无法读取响应
嵌入资源不受限<img><script><link><video> 等可跨域加载

注意:跨域请求实际上已经发出并到达了服务器,服务器也返回了响应,只是浏览器拦截了 JS 对响应的读取。这是同源策略的关键——它不是阻止请求发出,而是阻止读取响应。

嵌入资源的例外(不受同源限制):

  • <script src="..."> — 加载跨域 JS(JSONP 的基础)
  • <link href="..."> — 加载跨域 CSS
  • <img src="..."> — 加载跨域图片(但 Canvas 读取跨域图片数据会被污染)
  • <video> / <audio> — 加载跨域媒体
  • <iframe> — 嵌入跨域页面(但 JS 无法访问内部 DOM)

CORS(Cross-Origin Resource Sharing)

核心概念:

  • CORS:服务端设置 Access-Control-Allow-* 响应头,浏览器据此判断是否允许跨域,是 W3C 标准方案
  • 预检请求:非简单请求先发 OPTIONS 请求确认是否允许
  • 代理:开发时通过同源服务器转发请求,绕过浏览器限制

简单请求与非简单请求

简单请求的完整条件(必须同时满足):

  1. 方法:仅限 GETPOSTHEAD
  2. Header:仅限浏览器自动设置的 Header 和以下安全 Header:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type(仅限下述三种值)
    • DPRDownlinkSave-DataViewport-WidthWidth
  3. Content-Type:仅限以下三种:
    • text/plain
    • multipart/form-data
    • application/x-www-form-urlencoded
  4. 请求中没有使用 ReadableStream 对象
  5. 请求中没有注册任何事件监听器(XMLHttpRequest)

非简单请求(触发预检的条件,任一即触发):

  • 使用 PUTDELETEPATCH 等方法
  • 设置了自定义 Header(如 AuthorizationX-Token
  • Content-Typeapplication/json
  • 使用了 ReadableStream

预检请求完整流程

浏览器                                  服务器
  |                                      |
  |  1. OPTIONS /api/data                |
  |     Access-Control-Request-Method: PUT
  |     Access-Control-Request-Headers: Authorization, Content-Type
  |     Origin: https://frontend.com     |
  | ─────────────────────────────────────>|
  |                                      |
  |  2. 204 No Content                   |
  |     Access-Control-Allow-Origin: https://frontend.com
  |     Access-Control-Allow-Methods: GET, POST, PUT, DELETE
  |     Access-Control-Allow-Headers: Authorization, Content-Type
  |     Access-Control-Allow-Credentials: true
  |     Access-Control-Max-Age: 86400    |
  |<─────────────────────────────────────|
  |                                      |
  |  3. PUT /api/data                    |
  |     Authorization: Bearer xxx        |
  |     Content-Type: application/json   |
  |     Origin: https://frontend.com     |
  | ─────────────────────────────────────>|
  |                                      |
  |  4. 200 OK                           |
  |     Access-Control-Allow-Origin: https://frontend.com
  |     Access-Control-Allow-Credentials: true
  |<─────────────────────────────────────|

流程说明:

  1. 浏览器发现请求不满足简单请求条件,自动发出 OPTIONS 预检请求
  2. 服务器返回允许的方法、Header 和 Origin
  3. 预检通过后,浏览器发送实际请求
  4. 服务器返回实际响应(同样需要 CORS 头)
  5. 如果预检失败,浏览器不会发送实际请求,控制台报 CORS 错误

CORS 响应头完整列表

响应头说明示例
Access-Control-Allow-Origin允许的源,* 或具体 URLhttps://example.com
Access-Control-Allow-Methods允许的 HTTP 方法GET, POST, PUT, DELETE
Access-Control-Allow-Headers允许的请求 HeaderContent-Type, Authorization
Access-Control-Allow-Credentials是否允许携带 Cookietrue
Access-Control-Max-Age预检结果缓存时间(秒)86400
Access-Control-Expose-Headers允许 JS 读取的响应 HeaderX-Total-Count, X-Request-Id

关键细节:

  • Allow-OriginAllow-Credentials: true 不能同时使用 *,必须指定具体 Origin
  • Expose-Headers 默认只暴露 6 个简单响应头:Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma,自定义 Header 需要 Expose-Headers 声明才能被 JS 读取
  • Max-Age 缓存期间浏览器不会再次发送预检请求,不同浏览器有上限(Chrome 7200s / Firefox 86400s)

Cookie 的跨域行为非常特殊,需要理解以下属性:

属性作用跨域影响
DomainCookie 的域名作用域默认当前域名,不含子域;设置 .example.com 含所有子域
PathCookie 的路径作用域默认 /,仅匹配路径下发送
SameSite跨站发送策略Strict / Lax / None(见下表)
Secure仅 HTTPS 发送跨域 + HTTPS 场景必设
HttpOnlyJS 不可读防止 XSS 窃取,与跨域无直接关系

SameSite 属性详解:

SameSite 值跨站 Cookie 行为典型场景
Strict完全禁止跨站发送,包括从外部链接跳转银行等高安全场景
Lax(默认)允许顶级导航的 GET 请求携带,其余禁止大多数网站默认行为
None允许跨站发送,但必须同时设置 Secure第三方登录、嵌入式场景

从 Chrome 80 开始,SameSite 默认值从 None 改为 Lax,这意味着跨站 POST 请求、iframe 内请求、AJAX 请求都不会携带 Cookie,除非显式设置 SameSite=None; Secure

跨域携带 Cookie 的完整条件:

  1. 前端:fetch 设置 credentials: 'include',或 XMLHttpRequest 设置 withCredentials = true
  2. 后端:Access-Control-Allow-Credentials: true
  3. 后端:Access-Control-Allow-Origin 必须是具体值,不能是 *
  4. Cookie 属性:SameSite=None; Secure(跨站场景)
  5. Cookie 的 Domain 必须匹配请求域名

其他跨域方案

JSONP

原理: <script> 标签的 src 不受同源策略限制,服务器返回 callback(data) 形式的 JS 代码,前端通过回调函数获取数据。

局限: 只支持 GET、无状态码、XSS 风险、错误处理困难。

postMessage

原理: window.postMessage() 允许跨源窗口之间安全传递消息,是 iframe / window.open 跨域通信的标准方案。

局限: 仅限窗口间消息通信,不适用于 HTTP 请求。

WebSocket

原理: WebSocket 协议不受同源策略限制,建立连接时使用 HTTP Upgrade 握手,之后转为全双工通信。

局限: 需要服务端支持 WebSocket 协议。

document.domain(已废弃)

原理: 将两个页面的 document.domain 设置为相同的主域,可使其”同源”。

局限: 仅适用于子域之间(a.example.comb.example.com),Chrome 115+ 已废弃。

window.name

原理: window.name 在不同页面加载间保持不变,跨域 iframe 加载后通过 iframe.contentWindow.name 读取。

局限: 仅支持字符串数据,容量约 2MB,属于 Hack 方案,不推荐使用。

Why — 为什么

同源策略的安全意义

同源策略是浏览器最基础的安全防线,缺失将导致严重安全问题:

1. CSRF(跨站请求伪造)防护

没有同源策略,恶意网站 evil.com 可以用用户的 Cookie 向 bank.com 发起转账请求,并读取响应内容确认转账结果。同源策略阻止了 evil.com 的 JS 读取 bank.com 的响应,即使请求已被发出。

2. XSS 数据窃取防护

如果攻击者在 example.com 注入了恶意脚本,同源策略阻止该脚本读取 bank.com 的 DOM 和 Cookie(即使两个标签页同时打开),防止跨站数据窃取。

3. 数据隔离

不同源的页面互不干扰:

  • 用户在 gmail.com 的邮件内容不会被 evil.com 的 JS 读取
  • facebook.com 的登录状态不会被 tracker.com 直接获取
  • 不同公司的 SaaS 应用互不影响

本质:同源策略让”信任边界”以源为单位,一个源内的资源互相信任,跨源访问必须显式授权。

跨域场景分类

场景类型典型案例推荐方案
开发环境跨域localhost:3000localhost:8080Vite/Webpack 代理
生产环境-前后端分离app.example.comapi.example.comNginx 同源代理 / CORS
生产环境-第三方 API调用微信/支付宝开放平台CORS(由第三方配置)
微前端跨域主应用加载远程子应用postMessage / CORS
iframe 嵌入父页面与跨域 iframe 通信postMessage
实时通信跨域 WebSocket 连接WebSocket(无跨域限制)
旧系统兼容不支持 CORS 的老接口JSONP

方案完整对比

维度CORSNginx 代理JSONPpostMessageWebSocket
支持方法所有 HTTP所有 HTTP仅 GET消息通信全双工消息
改动量后端加 HeaderNginx 配置前后端改前端改前后端改
安全性好(浏览器校验)好(服务端控制)差(XSS 风险)中(需校验 Origin)
预检开销有(非简单请求)
浏览器兼容IE10+无限制IE6+IE8+IE10+
Cookie 支持需配置 Credentials同源自动携带需手动处理不涉及不涉及
适用场景通用 HTTP 请求开发/生产旧系统兼容iframe 通信实时双向通信

How — 怎么用

快速上手

后端 CORS 配置(Node.js Express):

const cors = require('cors');

// 允许所有源(开发环境)
app.use(cors());

// 生产环境:指定源
app.use(cors({
    origin: ['https://example.com', 'https://admin.example.com'],
    methods: ['GET', 'POST', 'PUT', 'DELETE'],
    allowedHeaders: ['Content-Type', 'Authorization'],
    credentials: true, // 允许携带 Cookie
    maxAge: 86400, // 预检缓存 24 小时
}));

代码示例

示例 1:前端代理(Vite 开发环境)

// vite.config.js
export default defineConfig({
    server: {
        proxy: {
            '/api': {
                target: 'http://localhost:8080',
                changeOrigin: true,
                rewrite: (path) => path.replace(/^\/api/, ''),
            },
        },
    },
});

Webpack 代理配置:

// webpack.config.js / vue.config.js
module.exports = {
    devServer: {
        proxy: {
            '/api': {
                target: 'http://localhost:8080',
                changeOrigin: true,
                pathRewrite: { '^/api': '' },
            },
        },
    },
};

示例 2:Nginx 反向代理(生产环境)

server {
    listen 80;
    server_name example.com;

    location / {
        root /var/www/html; # 前端静态文件
    }

    location /api/ {
        proxy_pass http://backend:8080/; # 转发到后端
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

示例 3:手动处理 CORS 预检

// 后端手动处理(无框架时)
app.use((req, res, next) => {
    res.header('Access-Control-Allow-Origin', 'https://example.com');
    res.header('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE');
    res.header('Access-Control-Allow-Headers', 'Content-Type,Authorization');
    res.header('Access-Control-Allow-Credentials', 'true');
    res.header('Access-Control-Max-Age', '86400');

    // 预检请求直接返回 204
    if (req.method === 'OPTIONS') {
        return res.sendStatus(204);
    }
    next();
});

示例 4:JSONP 完整实现(含 Promise 封装、超时处理、错误处理)

/**
 * JSONP 请求封装
 * @param {string} url - 请求地址
 * @param {object} params - 请求参数
 * @param {object} options - 配置项
 * @param {string} options.callbackName - 回调函数名(默认 jsonp_cb_${timestamp})
 * @param {number} options.timeout - 超时时间(默认 10000ms)
 * @returns {Promise<any>}
 */
function jsonp(url, params = {}, options = {}) {
    const {
        callbackName = `jsonp_cb_${Date.now()}_${Math.random().toString(36).slice(2)}`,
        timeout = 10000,
    } = options;

    return new Promise((resolve, reject) => {
        // 超时定时器
        const timer = setTimeout(() => {
            cleanup();
            reject(new Error(`JSONP request timeout: ${url}`));
        }, timeout);

        // 定义全局回调函数
        window[callbackName] = (data) => {
            cleanup();
            resolve(data);
        };

        // 清理函数:移除 script 标签和全局回调
        function cleanup() {
            clearTimeout(timer);
            delete window[callbackName];
            if (script.parentNode) {
                script.parentNode.removeChild(script);
            }
        }

        // 构建 URL
        const queryStr = Object.entries({ ...params, callback: callbackName })
            .map(([key, val]) => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`)
            .join('&');
        const fullUrl = `${url}?${queryStr}`;

        // 创建 script 标签
        const script = document.createElement('script');
        script.src = fullUrl;

        script.onerror = () => {
            cleanup();
            reject(new Error(`JSONP request failed: ${url}`));
        };

        // 添加到 DOM 触发请求
        document.head.appendChild(script);
    });
}

// 使用示例
jsonp('https://api.example.com/data', { id: 1 }, { timeout: 5000 })
    .then((data) => console.log('数据:', data))
    .catch((err) => console.error('错误:', err));

// Node.js 后端配合
app.get('/api/data', (req, res) => {
    const callback = req.query.callback;
    const data = { name: 'Alice', age: 25 };
    // 返回 callback(data) 形式的 JS
    res.type('text/javascript').send(`${callback}(${JSON.stringify(data)})`);
});

示例 5:postMessage 跨窗口通信

iframe 父子页面通信:

<!-- 父页面:https://parent.com -->
<iframe id="child" src="https://child.com/page"></iframe>

<script>
const iframe = document.getElementById('child');

// 向子页面发送消息
iframe.onload = () => {
    iframe.contentWindow.postMessage(
        { type: 'PARENT_MSG', data: 'Hello from parent' },
        'https://child.com' // 必须指定目标 Origin,不要用 '*'
    );
};

// 接收子页面消息
window.addEventListener('message', (event) => {
    // 安全校验:验证 Origin
    if (event.origin !== 'https://child.com') return;

    console.log('收到子页面消息:', event.data);
});
</script>
<!-- 子页面:https://child.com/page -->
<script>
// 接收父页面消息
window.addEventListener('message', (event) => {
    // 安全校验:验证 Origin
    if (event.origin !== 'https://parent.com') return;

    console.log('收到父页面消息:', event.data);

    // 向父页面回复消息
    event.source.postMessage(
        { type: 'CHILD_REPLY', data: 'Hello from child' },
        event.origin
    );
});
</script>

window.open 跨窗口通信:

// 打开新窗口
const popup = window.open('https://other.com/page', '_blank', 'width=600,height=400');

// 向新窗口发送消息
popup.postMessage({ type: 'MAIN_MSG', data: 'Hello' }, 'https://other.com');

// 新窗口中接收
window.addEventListener('message', (event) => {
    if (event.origin !== 'https://main.com') return;
    console.log('收到主窗口消息:', event.data);
});

安全要点:发送时始终指定 targetOrigin,接收时始终校验 event.origin,不要使用 '*'

示例 6:WebSocket 跨域通信

// 前端:WebSocket 连接不受同源策略限制
const ws = new WebSocket('wss://api.example.com/ws');

ws.onopen = () => {
    console.log('WebSocket 连接已建立');
    ws.send(JSON.stringify({ type: 'greeting', data: 'Hello' }));
};

ws.onmessage = (event) => {
    const data = JSON.parse(event.data);
    console.log('收到消息:', data);
};

ws.onerror = (error) => {
    console.error('WebSocket 错误:', error);
};

ws.onclose = (event) => {
    console.log('WebSocket 关闭:', event.code, event.reason);
};
// Node.js 后端(ws 库)
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws, req) => {
    // 可通过 req.headers.origin 校验来源
    const origin = req.headers.origin;
    const allowedOrigins = ['https://example.com', 'https://admin.example.com'];

    if (!allowedOrigins.includes(origin)) {
        ws.close(4001, 'Origin not allowed');
        return;
    }

    ws.on('message', (message) => {
        console.log('收到:', message.toString());
        ws.send(JSON.stringify({ type: 'echo', data: message.toString() }));
    });
});

示例 7:CORS 中间件生产级配置(动态 Origin 白名单)

// corsMiddleware.js — 生产级 CORS 中间件
const allowedOrigins = [
    'https://www.example.com',
    'https://admin.example.com',
    'https://m.example.com',
];

// 支持正则匹配(适合多环境部署)
const originRegex = /^https:\/\/[a-z0-9-]+\.example\.com$/;

function corsMiddleware(req, res, next) {
    const origin = req.headers.origin;

    if (!origin) {
        // 非浏览器请求或同源请求,无需 CORS
        return next();
    }

    // 动态判断 Origin 是否允许
    const isAllowed = allowedOrigins.includes(origin) || originRegex.test(origin);

    if (!isAllowed) {
        return res.status(403).json({ error: 'CORS origin not allowed' });
    }

    // 设置 CORS 响应头
    res.header('Access-Control-Allow-Origin', origin); // 动态设置,不用 *
    res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
    res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With');
    res.header('Access-Control-Allow-Credentials', 'true');
    res.header('Access-Control-Max-Age', '86400');

    // 暴露自定义响应头供前端读取
    res.header('Access-Control-Expose-Headers', 'X-Total-Count, X-Request-Id');

    // 预检请求直接返回
    if (req.method === 'OPTIONS') {
        return res.sendStatus(204);
    }

    next();
}

module.exports = corsMiddleware;

// 使用
app.use(corsMiddleware);

示例 8:Fetch API credentials 三种模式

// 1. same-origin(默认):同源请求携带 Cookie,跨域不携带
fetch('https://api.example.com/user', {
    credentials: 'same-origin',
});

// 2. include:无论同源跨域都携带 Cookie
fetch('https://api.example.com/user', {
    credentials: 'include',
    headers: { 'Content-Type': 'application/json' },
});

// 3. omit:任何请求都不携带 Cookie
fetch('https://api.example.com/public-data', {
    credentials: 'omit',
});

// XMLHttpRequest 对比
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/user');
xhr.withCredentials = true; // 等同于 credentials: 'include'
xhr.send();
credentials 模式同源跨域典型场景
same-origin携带不携带默认行为,适合大多数场景
include携带携带跨域携带 Cookie 登录态
omit不携带不携带公开 API,无需认证

示例 9:Nginx 完整生产配置(含 HTTPS、CORS Header、代理)

# HTTP → HTTPS 重定向
server {
    listen 80;
    server_name example.com;
    return 301 https://$server_name$request_uri;
}

# HTTPS 主配置
server {
    listen 443 ssl http2;
    server_name example.com;

    # SSL 证书
    ssl_certificate     /etc/nginx/ssl/example.com.crt;
    ssl_certificate_key /etc/nginx/ssl/example.com.key;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;

    # 安全 Header
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    # 前端静态资源
    location / {
        root /var/www/html;
        try_files $uri $uri/ /index.html; # SPA 路由回退

        # 静态资源缓存
        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
            expires 30d;
            add_header Cache-Control "public, immutable";
        }
    }

    # API 代理(同源方案,浏览器无跨域)
    location /api/ {
        proxy_pass http://backend:8080/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # WebSocket 代理支持
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        # 超时配置
        proxy_connect_timeout 60s;
        proxy_read_timeout 120s;
        proxy_send_timeout 60s;
    }

    # 如果 API 与前端不同域,需要 CORS Header
    location /external-api/ {
        proxy_pass http://backend:8080/;

        # CORS Header(由 Nginx 添加,无需后端处理)
        add_header Access-Control-Allow-Origin "https://example.com" always;
        add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
        add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
        add_header Access-Control-Allow-Credentials "true" always;
        add_header Access-Control-Max-Age 86400 always;

        # 预检请求处理
        if ($request_method = OPTIONS) {
            return 204;
        }

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

示例 10:跨域上传文件(FormData + CORS)

// 前端:跨域上传文件
async function uploadFile(file) {
    const formData = new FormData();
    formData.append('file', file);
    formData.append('userId', '123');

    try {
        const response = await fetch('https://api.example.com/upload', {
            method: 'POST',
            body: formData, // 不设 Content-Type,浏览器自动设 multipart/form-data + boundary
            credentials: 'include', // 携带 Cookie
        });

        if (!response.ok) {
            throw new Error(`上传失败: ${response.status}`);
        }

        const result = await response.json();
        console.log('上传成功:', result);
        return result;
    } catch (error) {
        console.error('上传错误:', error);
        throw error;
    }
}

// 使用
const input = document.querySelector('input[type="file"]');
input.addEventListener('change', (e) => {
    uploadFile(e.target.files[0]);
});
// 后端:Express 处理跨域文件上传
const multer = require('multer');
const upload = multer({ dest: 'uploads/', limits: { fileSize: 10 * 1024 * 1024 } }); // 10MB 限制

app.post('/upload', upload.single('file'), (req, res) => {
    if (!req.file) {
        return res.status(400).json({ error: 'No file uploaded' });
    }
    res.json({
        filename: req.file.filename,
        size: req.file.size,
        mimetype: req.file.mimetype,
    });
});

注意:上传文件时不要手动设置 Content-Type: multipart/form-data,浏览器会自动设置带 boundary 的完整 Content-Type,手动设置会导致服务端无法解析。

常见问题与踩坑

问题原因解决方案
预检请求失败后端未处理 OPTIONS添加 OPTIONS 处理,返回 204
Cookie 不发送withCredentials 未设置前端 fetchcredentials: 'include',后端 Allow-Credentials: true
Allow-Origin 不能用 * + Credentials浏览器安全限制必须指定具体 Origin
部分接口跨域部分不跨域只需要某些路由跨域按路由设置 CORS,而非全局
自定义响应头前端读不到默认只暴露 6 个简单头后端设置 Access-Control-Expose-Headers
301/302 重定向后 CORS 丢失重定向后 Origin 变为 null避免跨域请求中的重定向,或在重定向目标也配置 CORS
SameSite=Lax 阻止跨站 CookieChrome 80+ 默认值变更跨站场景设置 SameSite=None; Secure
CORS Header 被多次 add_header 覆盖Nginx 的 add_header 在 if 块中覆盖外层使用 always 参数,避免嵌套 if
预检请求耗时过长每次都发 OPTIONS设置 Access-Control-Max-Age 缓存预检结果
本地文件跨域file:// 协议的 Origin 为 null使用本地 HTTP 服务器开发,不用 file:// 直接打开

最佳实践

  • 生产用 Nginx 同源代理,开发用 Vite proxy
  • CORS 指定具体 Origin,不用 *
  • 预检缓存 Max-Age 减少 OPTIONS 请求
  • 需要 Cookie 时 credentials: 'include' + 后端 Allow-Credentials
  • postMessage 始终校验 origin,不用 '*'
  • JSONP 仅用于旧系统兼容,新项目用 CORS
  • Nginx 代理场景添加 X-Forwarded-For / X-Real-IP 保留客户端信息
  • 文件上传不要手动设 Content-Type

面试题

Q1: 什么是同源策略?为什么需要它?

同源策略要求协议、域名、端口完全一致才同源。它限制跨源 JS 读取 DOM、Cookie 和网络响应,防止恶意网站窃取用户在其他站点的数据,是浏览器最基础的安全机制。注意:跨域请求实际上已发出并到达服务器,只是浏览器拦截了响应。

Q2: CORS 的预检请求是什么?什么情况下会触发?

预检请求是浏览器在发送跨域”非简单请求”前自动发出的 OPTIONS 请求,用于确认服务器是否允许实际请求。触发条件:使用了 PUT/DELETE/PATCH 等非简单方法、自定义 Header、Content-Type 非 application/x-www-form-urlencoded/multipart/form-data/text/plain。可通过 Access-Control-Max-Age 缓存预检结果减少请求。

Q3: JSONP 的原理和局限性是什么?

JSONP 利用 <script> 标签不受同源策略限制的特点,动态创建 script 标签请求带回调函数名的 URL,服务器返回 callback(data) 形式的 JS 执行。局限性:只支持 GET 请求、无法获取响应状态码、存在 XSS 安全风险、错误处理困难。现代项目应优先使用 CORS。

Q4: 为什么生产环境推荐 Nginx 反向代理解决跨域?

Nginx 代理让前端和 API 同源,浏览器感知不到跨域,无需 CORS 配置,避免了预检请求的额外开销。同时 Nginx 可统一管理负载均衡、限流、缓存等,前后端部署解耦。但需注意代理会增加一跳网络延迟。

Q5: Cookie 的 SameSite 属性对跨域有什么影响?

SameSite 控制跨站请求是否携带 Cookie:Strict 完全禁止跨站发送(包括从外部链接跳转);Lax(Chrome 80+ 默认)允许顶级导航 GET 请求携带,其余禁止;None 允许跨站发送但必须配合 Secure(仅 HTTPS)。跨域 AJAX 携带 Cookie 需 SameSite=None; Secure,否则浏览器不会在跨站请求中附上 Cookie。

Q6: postMessage 如何安全地进行跨窗口通信?

发送方必须指定 targetOrigin(不要用 '*'),接收方必须校验 event.origin 是否在白名单内,同时验证 event.data 的结构和类型。典型模式:父页面 iframe.contentWindow.postMessage(data, 'https://child.com'),子页面 window.addEventListener('message', (e) => { if (e.origin !== 'https://parent.com') return; ... })。还应注意校验消息格式,防止恶意数据注入。

Q7: 为什么 WebSocket 不受同源策略限制?

WebSocket 设计为全双工通信协议,其连接建立通过 HTTP Upgrade 握手完成,但握手完成后不再使用 HTTP 协议。同源策略是浏览器对 HTTP 请求/响应的安全限制,WebSocket 作为独立协议不在其约束范围内。不过服务端仍可通过 Origin Header 校验来决定是否接受连接,浏览器也会在连接时携带 Origin 信息。

Q8: 如何区分简单请求和非简单请求?

简单请求必须同时满足:① 方法为 GET/POST/HEAD;② Content-Type 仅限 text/plainmultipart/form-dataapplication/x-www-form-urlencoded;③ 不包含自定义 Header;④ 没有使用 ReadableStream。任一条件不满足即为非简单请求,浏览器会先发 OPTIONS 预检。实际开发中 application/json + Authorization Header 是最常见的触发预检组合。


相关链接: