跨域问题:从同源策略到现代解决方案深度解析

178 阅读5分钟

一、同源策略:浏览器安全的基石

1.1 什么是同源策略

同源策略(Same-Origin Policy)是现代浏览器最基本、最重要的安全策略之一。它规定了一个源(协议、域名、端口号完全相同的URL)的文档或脚本如何与另一个源的资源进行交互。当两个URL的协议、域名、端口号中有任何一项不同时,就属于不同的源,浏览器会默认阻止跨源请求。

同源判断示例:

  • https://www.example.com/index.html → https://www.example.com/api/data ✅ 同源
  • https://www.example.com/index.html → http://www.example.com/api/data ❌ 协议不同
  • https://www.example.com/index.html → https://api.example.com/data ❌ 域名不同
  • https://www.example.com/index.html → https://www.example.com:8080/data ❌ 端口不同

1.2 同源策略的限制范围

同源策略主要限制以下三类行为:

  1. DOM访问限制:禁止不同源页面之间的DOM访问
  2. AJAX请求限制:禁止XMLHttpRequest或Fetch API发送跨域请求
  3. Cookie、LocalStorage、IndexedDB等存储限制:禁止访问不同源的存储数据

1.3 为什么需要同源策略

如果没有同源策略,恶意网站可能会:

  • 通过iframe嵌入银行网站,读取用户的账户信息
  • 发送AJAX请求到其他网站,使用用户的登录凭证进行操作
  • 读取其他网站的本地存储数据,获取敏感信息

二、JSONP:早期的跨域解决方案

2.1 JSONP工作原理

JSONP(JSON with Padding)利用了<script>标签不受同源策略限制的特性。浏览器允许通过<script>标签加载任何来源的JavaScript文件。

核心原理:

  1. 前端创建一个<script>标签,将请求URL作为src属性
  2. URL中包含一个回调函数名作为参数
  3. 服务器返回的不是JSON,而是JavaScript代码:callbackFunction(jsonData)
  4. 浏览器执行返回的JavaScript代码,调用前端定义的回调函数

2.2 完整实现示例

前端实现:

javascript

复制下载

function jsonp(url, callbackName) {
    return new Promise((resolve, reject) => {
        // 创建script标签
        const script = document.createElement('script');
        
        // 定义全局回调函数
        window[callbackName] = (data) => {
            resolve(data);
            document.body.removeChild(script);
            delete window[callbackName];
        };
        
        // 设置超时处理
        script.timeout = 5000;
        script.onerror = () => {
            reject(new Error('JSONP request failed'));
            document.body.removeChild(script);
            delete window[callbackName];
        };
        
        // 设置src并添加到文档
        script.src = `${url}${url.includes('?') ? '&' : '?'}callback=${callbackName}`;
        document.body.appendChild(script);
    });
}

// 使用示例
jsonp('https://api.example.com/data', 'handleData')
    .then(data => console.log(data))
    .catch(err => console.error(err));

服务器端实现(Node.js):

javascript

复制下载

const http = require('http');
const url = require('url');

const server = http.createServer((req, res) => {
    const parsedUrl = url.parse(req.url, true);
    
    if (parsedUrl.pathname === '/api/data') {
        const callback = parsedUrl.query.callback;
        const data = JSON.stringify({ message: 'Hello from JSONP' });
        
        // 设置响应头
        res.writeHead(200, {
            'Content-Type': 'application/javascript'
        });
        
        // 返回JavaScript代码
        res.end(`${callback}(${data})`);
    }
});

server.listen(3000);

2.3 JSONP的优缺点分析

优点:

  • 兼容性好,支持老旧浏览器
  • 实现简单,无需服务器特殊配置
  • 可以绕过CORS限制

缺点:

  • 只支持GET请求,无法使用POST、PUT等HTTP方法
  • 安全性问题:容易受到XSS攻击
  • 错误处理困难:无法使用HTTP状态码进行错误处理
  • 缺乏标准化:需要前后端约定回调函数名

三、CORS:现代跨域解决方案的标准

3.1 CORS工作原理

CORS(Cross-Origin Resource Sharing)是一个W3C标准,它允许服务器声明哪些源可以访问资源。当浏览器检测到跨域请求时,会自动添加Origin头部,服务器通过响应头决定是否允许该请求。

3.2 简单请求与预检请求

3.2.1 简单请求(Simple Request)

满足以下所有条件的请求为简单请求:

  1. 使用GET、HEAD、POST方法之一
  2. 仅包含以下头部:Accept、Accept-Language、Content-Language、Content-Type
  3. Content-Type为以下之一:text/plain、multipart/form-data、application/x-www-form-urlencoded

简单请求流程:

javascript

复制下载

// 浏览器自动添加Origin头部
GET /api/data HTTP/1.1
Origin: https://www.example.com
Host: api.example.com

// 服务器响应
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.example.com
Content-Type: application/json

{"data": "some data"}

3.2.2 预检请求(Preflight Request)

不满足简单请求条件的请求会先发送OPTIONS预检请求:

预检请求示例:

javascript

复制下载

// 预检请求
OPTIONS /api/data HTTP/1.1
Origin: https://www.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header

// 预检响应
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Max-Age: 86400

// 实际请求
PUT /api/data HTTP/1.1
Origin: https://www.example.com
X-Custom-Header: value

3.3 完整的CORS配置

Node.js Express服务器配置:

javascript

复制下载

const express = require('express');
const app = express();

// CORS中间件
const corsMiddleware = (req, res, next) => {
    // 允许的来源
    const allowedOrigins = [
        'https://www.example.com',
        'https://admin.example.com'
    ];
    const origin = req.headers.origin;
    
    if (allowedOrigins.includes(origin)) {
        res.setHeader('Access-Control-Allow-Origin', origin);
    }
    
    // 允许的HTTP方法
    res.setHeader('Access-Control-Allow-Methods', 
        'GET, POST, PUT, DELETE, OPTIONS, PATCH');
    
    // 允许的请求头
    res.setHeader('Access-Control-Allow-Headers',
        'Content-Type, Authorization, X-Requested-With, X-Custom-Header');
    
    // 允许携带凭证(cookies、HTTP认证等)
    res.setHeader('Access-Control-Allow-Credentials', 'true');
    
    // 预检请求缓存时间(秒)
    res.setHeader('Access-Control-Max-Age', '86400');
    
    // 暴露给前端的响应头
    res.setHeader('Access-Control-Expose-Headers',
        'X-Total-Count, X-Custom-Header');
    
    // 如果是预检请求,直接返回200
    if (req.method === 'OPTIONS') {
        return res.status(200).end();
    }
    
    next();
};

app.use(corsMiddleware);

// 路由示例
app.post('/api/login', (req, res) => {
    // 处理登录逻辑
    res.json({ success: true, token: 'jwt-token' });
});

app.listen(3000);

Spring Boot配置:

java

复制下载

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins("https://www.example.com", 
                                "https://admin.example.com")
                .allowedMethods("GET", "POST", "PUT", "DELETE", 
                                "OPTIONS", "PATCH")
                .allowedHeaders("*")
                .exposedHeaders("X-Total-Count", "X-Custom-Header")
                .allowCredentials(true)
                .maxAge(86400L);
    }
}

3.4 带凭证的CORS请求

当请求需要携带cookies或HTTP认证信息时:

前端配置:

javascript

复制下载

// Fetch API
fetch('https://api.example.com/protected-data', {
    method: 'GET',
    credentials: 'include',  // 必须设置为include
    headers: {
        'Content-Type': 'application/json'
    }
});

// XMLHttpRequest
const xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.open('GET', 'https://api.example.com/protected-data');
xhr.send();

服务器端必须响应:

text

复制下载

Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://www.example.com  // 不能使用通配符*

四、服务器代理:开发环境的跨域解决方案

4.1 正向代理 vs 反向代理

正向代理:代理客户端向服务器发出请求,隐藏客户端身份
反向代理:代理服务器接收客户端请求,隐藏服务器身份

4.2 开发环境代理配置

Webpack Dev Server配置:

javascript

复制下载

// webpack.config.js
module.exports = {
    // ...其他配置
    devServer: {
        proxy: {
            '/api': {
                target: 'https://api.example.com',
                changeOrigin: true,
                secure: false,
                pathRewrite: {
                    '^/api': ''  // 移除路径中的/api前缀
                },
                onProxyReq: (proxyReq, req, res) => {
                    // 可以在这里修改请求头
                    proxyReq.setHeader('X-Forwarded-For', req.ip);
                },
                onProxyRes: (proxyRes, req, res) => {
                    // 可以在这里修改响应头
                    proxyRes.headers['X-Proxy'] = 'webpack-dev-server';
                }
            }
        }
    }
};

Node.js代理服务器实现:

javascript

复制下载

const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');

const app = express();

// 静态文件服务
app.use(express.static('public'));

// 代理配置
const apiProxy = createProxyMiddleware({
    target: 'https://api.example.com',
    changeOrigin: true,
    pathRewrite: {
        '^/proxy/api': '/api'  // /proxy/api/users -> /api/users
    },
    onError: (err, req, res) => {
        console.error('Proxy error:', err);
        res.status(500).json({ error: 'Proxy error' });
    },
    // 自定义请求头
    headers: {
        'X-Proxy-Server': 'node-proxy'
    }
});

app.use('/proxy/api', apiProxy);

// 健康检查端点
app.get('/health', (req, res) => {
    res.json({ status: 'healthy' });
});

app.listen(8080, () => {
    console.log('Proxy server running on http://localhost:8080');
});

4.3 Nginx反向代理配置

生产环境Nginx配置:

nginx

复制下载

server {
    listen 80;
    server_name www.example.com;
    
    # 前端静态文件
    location / {
        root /usr/share/nginx/html;
        index index.html;
        try_files $uri $uri/ /index.html;
    }
    
    # API代理配置
    location /api/ {
        # 反向代理到后端服务器
        proxy_pass http://backend-server:3000/;
        
        # CORS相关配置
        add_header 'Access-Control-Allow-Origin' 'https://www.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;
        
        # 处理预检请求
        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Max-Age' 86400;
            add_header 'Content-Type' 'text/plain charset=UTF-8';
            add_header 'Content-Length' 0;
            return 204;
        }
        
        # 代理配置
        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;
        
        # 超时设置
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }
    
    # WebSocket代理配置
    location /ws/ {
        proxy_pass http://backend-server:3000/ws/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
    }
}

五、安全考虑与最佳实践

5.1 安全风险

  1. CSRF攻击:CORS不会防止CSRF攻击,需要配合CSRF Token
  2. 信息泄露:错误的CORS配置可能导致敏感信息泄露
  3. DNS重绑定攻击:攻击者可能利用DNS重绑定绕过同源策略

5.2 安全配置建议

javascript

复制下载

// 安全的CORS配置
const corsOptions = {
    // 严格限制允许的来源
    origin: (origin, callback) => {
        const allowedOrigins = [
            'https://www.example.com',
            'https://app.example.com'
        ];
        
        // 不允许没有origin的请求(如curl、postman)
        if (!origin) return callback(new Error('Not allowed'), false);
        
        if (allowedOrigins.indexOf(origin) === -1) {
            return callback(new Error('Not allowed by CORS'), false);
        }
        
        return callback(null, true);
    },
    
    // 限制允许的方法
    methods: ['GET', 'POST', 'PUT', 'DELETE'],
    
    // 限制允许的头部
    allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-ID'],
    
    // 限制暴露的头部
    exposedHeaders: ['X-Total-Count'],
    
    // 启用凭证
    credentials: true,
    
    // 设置预检缓存
    maxAge: 86400,
    
    // 设置预检响应状态码
    optionsSuccessStatus: 204
};

5.3 生产环境最佳实践

  1. 使用白名单机制:不要使用Access-Control-Allow-Origin: *
  2. 限制HTTP方法:只允许必要的HTTP方法
  3. 实施速率限制:防止API被滥用
  4. 使用HTTPS:确保所有跨域请求都通过HTTPS
  5. 监控和日志:记录所有跨域请求,便于审计
  6. 定期安全审计:检查CORS配置是否符合安全要求

七、面试重点总结

7.1 必考知识点

  1. 同源策略的定义和目的

    • 协议、域名、端口相同即为同源
    • 主要目的是防止恶意网站窃取数据
  2. JSONP的原理和局限性

    • 利用<script>标签不受同源策略限制
    • 只支持GET请求,安全性较差
  3. CORS的工作机制

    • 简单请求 vs 预检请求
    • 服务器通过响应头控制访问权限
    • credentials: 'include'Access-Control-Allow-Credentials
  4. 代理服务器的应用场景

    • 开发环境解决跨域
    • 生产环境的负载均衡和安全防护

7.2 常见面试题

  1. "CORS中简单请求和预检请求的区别是什么?"
  2. "如何实现安全的CORS配置?"
  3. "JSONP和CORS各有什么优缺点?"
  4. "什么情况下应该使用代理服务器?"
  5. "如何防止CORS配置不当导致的安全问题?"

7.3 实际应用建议

  1. 现代应用首选CORS:大多数场景下,CORS是最佳选择
  2. 遗留系统考虑JSONP:仅用于支持老旧浏览器
  3. 开发环境使用代理:方便前端开发调试
  4. 生产环境结合使用:CORS + 反向代理提供更好的安全性和性能

通过深入理解同源策略和各种跨域解决方案的原理、实现方式及安全考虑,开发者可以根据具体场景选择最合适的跨域方案,既保证功能实现,又确保应用安全。在现代Web开发中,掌握这些知识不仅是解决跨域问题的基础,更是构建安全、可靠Web应用的重要保障。