关于跨域,你到底知道多少?

146 阅读5分钟

跨域解决方案完全指南

目录

  1. 什么是跨域
  2. 同源策略
  3. CORS解决方案
  4. JSONP解决方案
  5. 代理服务器解决方案
  6. Websocket解决方案
  7. postMessage解决方案
  8. document.domain解决方案
  9. 最佳实践与注意事项

什么是跨域

1.1 跨域的定义

跨域是指浏览器限制当前页面的脚本不能访问其他源的资源。出现跨域的条件是:

  • 协议不同(http vs https)
  • 域名不同(example.com vs api.example.com)
  • 端口不同(localhost:3000 vs localhost:8080)

image.png

image.png

1.2 跨域常见场景

  1. 前端与后端分离部署

    • 前端:http://localhost:3000(React/Vue开发服务器)
    • 后端:http://localhost:8080(SpringBoot/Django)
  2. 调用第三方API

    • 你的网站:https://your-site.com
    • 第三方API:https://api.weibo.com
  3. CDN资源加载

    • 主站:https://www.example.com
    • CDN资源:https://cdn.example.net/static/js/app.js

同源策略

2.1 同源策略定义

同源策略是浏览器的一个重要的安全策略,用于限制一个源的文档或者它加载的脚本如何能与另一个源的资源进行交互。

2.2 同源策略限制内容

// 1. Cookie、LocalStorage和IndexDB无法读取
// 2. DOM和JS对象无法获得
// 3. AJAX请求不能发送

CORS解决方案

image.png

3.1 前端实现

// 1. 简单请求
fetch('https://api.example.com/data', {
  method: 'GET',
  credentials: 'include',  // 携带Cookie
  headers: {
    'Content-Type': 'application/json'
  }
});

// 2. 预检请求
fetch('https://api.example.com/data', {
  method: 'PUT',
  credentials: 'include',
  headers: {
    'Content-Type': 'application/json',
    'X-Custom-Header': 'value'
  },
  body: JSON.stringify({ name: 'test' })
});

// 3. axios配置
axios.create({
  baseURL: 'https://api.example.com',
  withCredentials: true,
  headers: {
    'Content-Type': 'application/json'
  }
});

3.2 后端实现

3.2.1 Node.js (Express) 实现
const express = require('express');
const cors = require('cors');
const app = express();

// 方式1:使用cors中间件(推荐)
const corsOptions = {
  origin: function(origin, callback) {
    const allowedOrigins = ['https://example.com', 'https://www.example.com'];
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('不允许的跨域请求'));
    }
  },
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-Custom-Header'],
  exposedHeaders: ['Content-Range', 'X-Content-Range'],
  credentials: true,
  maxAge: 86400
};

app.use(cors(corsOptions));

// 方式2:手动设置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,OPTIONS');
  res.header('Access-Control-Allow-Headers', 'Content-Type,Authorization');
  res.header('Access-Control-Allow-Credentials', 'true');
  res.header('Access-Control-Max-Age', '86400');
  
  if (req.method === 'OPTIONS') {
    return res.sendStatus(200);
  }
  next();
});
3.2.2 Python (Django) 实现
# settings.py
INSTALLED_APPS = [
    ...
    'corsheaders',
]

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.common.CommonMiddleware',
    ...
]

CORS_ALLOWED_ORIGINS = [
    "https://example.com",
    "https://www.example.com",
]

CORS_ALLOW_METHODS = [
    'DELETE',
    'GET',
    'OPTIONS',
    'PATCH',
    'POST',
    'PUT',
]

CORS_ALLOW_HEADERS = [
    'accept',
    'accept-encoding',
    'authorization',
    'content-type',
    'dnt',
    'origin',
    'user-agent',
    'x-csrftoken',
    'x-requested-with',
]

CORS_ALLOW_CREDENTIALS = True
CORS_PREFLIGHT_MAX_AGE = 86400
3.2.3 Java (Spring Boot) 实现
@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("https://example.com")
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(86400);
    }
}

// 或者使用注解方式
@CrossOrigin(
    origins = "https://example.com",
    methods = {RequestMethod.GET, RequestMethod.POST},
    allowedHeaders = "*",
    allowCredentials = "true",
    maxAge = 86400
)
@RestController
public class ApiController {
    // 控制器方法
}

JSONP解决方案

4.1 前端实现

// 1. 原生JS实现
function jsonp(url, callback) {
  const script = document.createElement('script');
  const callbackName = 'jsonp_' + Date.now();
  
  // 创建全局回调函数
  window[callbackName] = function(data) {
    callback(data);
    document.body.removeChild(script);
    delete window[callbackName];
  };
  
  // 添加script标签
  script.src = `${url}${url.includes('?') ? '&' : '?'}callback=${callbackName}`;
  document.body.appendChild(script);
}

// 使用示例
jsonp('https://api.example.com/data', function(data) {
  console.log('收到数据:', data);
});

// 2. jQuery实现
$.ajax({
  url: 'https://api.example.com/data',
  dataType: 'jsonp',
  jsonp: 'callback',
  success: function(data) {
    console.log('收到数据:', data);
  }
});

4.2 后端实现

4.2.1 Node.js实现
const express = require('express');
const app = express();

app.get('/api/data', (req, res) => {
  const data = {
    name: 'test',
    value: 123
  };
  
  const callback = req.query.callback;
  if (callback) {
    res.type('text/javascript');
    res.send(`${callback}(${JSON.stringify(data)})`);
  } else {
    res.json(data);
  }
});
4.2.2 Python实现
from django.http import HttpResponse
import json

def api_data(request):
    data = {
        'name': 'test',
        'value': 123
    }
    
    callback = request.GET.get('callback')
    if callback:
        response_data = f"{callback}({json.dumps(data)})"
        return HttpResponse(response_data, content_type="text/javascript")
    return JsonResponse(data)

代理服务器解决方案

5.1 开发环境代理

5.1.1 Vue项目配置
// vue.config.js
module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: 'https://api.example.com',
        changeOrigin: true,
        pathRewrite: {
          '^/api': ''
        }
      }
    }
  }
}
5.1.2 React项目配置
// package.json
{
  "proxy": "https://api.example.com"
}

// 或者使用setupProxy.js
const { createProxyMiddleware } = require('http-proxy-middleware');

module.exports = function(app) {
  app.use(
    '/api',
    createProxyMiddleware({
      target: 'https://api.example.com',
      changeOrigin: true,
      pathRewrite: {
        '^/api': ''
      }
    })
  );
};

5.2 生产环境代理

5.2.1 Nginx配置
# nginx.conf
server {
    listen 80;
    server_name example.com;

    # 静态资源
    location / {
        root /usr/share/nginx/html;
        index index.html index.htm;
        try_files $uri $uri/ /index.html;
    }

    # API代理
    location /api/ {
        proxy_pass https://api.example.com/;
        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;
        
        # 允许跨域配置
        add_header Access-Control-Allow-Origin $http_origin;
        add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS';
        add_header Access-Control-Allow-Headers 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
        add_header Access-Control-Allow-Credentials 'true';
        
        if ($request_method = 'OPTIONS') {
            add_header Access-Control-Max-Age 86400;
            add_header Content-Length 0;
            add_header Content-Type text/plain;
            return 204;
        }
    }
}

Websocket解决方案

6.1 前端实现

// 1. 原生WebSocket
const ws = new WebSocket('wss://api.example.com/ws');

ws.onopen = () => {
  console.log('连接已建立');
  ws.send(JSON.stringify({ type: 'hello' }));
};

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

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

ws.onclose = () => {
  console.log('连接已关闭');
};

// 2. Socket.io
const socket = io('https://api.example.com', {
  withCredentials: true,
  transportOptions: {
    polling: {
      extraHeaders: {
        'Authorization': 'Bearer token'
      }
    }
  }
});

socket.on('connect', () => {
  console.log('连接已建立');
});

socket.on('message', (data) => {
  console.log('收到消息:', data);
});

socket.on('disconnect', () => {
  console.log('连接已断开');
});

6.2 后端实现

6.2.1 Node.js (ws) 实现
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws) => {
  console.log('新的连接');

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

  ws.on('close', () => {
    console.log('连接关闭');
  });
});
6.2.2 Node.js (Socket.io) 实现
const express = require('express');
const app = express();
const server = require('http').createServer(app);
const io = require('socket.io')(server, {
  cors: {
    origin: "https://example.com",
    methods: ["GET", "POST"],
    allowedHeaders: ["Authorization"],
    credentials: true
  }
});

io.on('connection', (socket) => {
  console.log('用户连接:', socket.id);

  socket.on('message', (data) => {
    console.log('收到消息:', data);
    socket.emit('response', { type: 'response', data: '收到消息' });
  });

  socket.on('disconnect', () => {
    console.log('用户断开连接:', socket.id);
  });
});

server.listen(3000);

postMessage解决方案

7.1 发送消息

// 父窗口发送消息
const iframe = document.querySelector('iframe');
iframe.onload = () => {
  iframe.contentWindow.postMessage({
    type: 'getData',
    data: { id: 1 }
  }, 'https://example.com');
};

// 子窗口发送消息
window.parent.postMessage({
  type: 'response',
  data: { result: 'success' }
}, 'https://parent-domain.com');

7.2 接收消息

// 监听消息
window.addEventListener('message', (event) => {
  // 验证来源
  if (event.origin !== 'https://trusted-domain.com') {
    return;
  }
  
  const { type, data } = event.data;
  
  switch (type) {
    case 'getData':
      // 处理获取数据请求
      handleGetData(data);
      break;
    case 'response':
      // 处理响应
      handleResponse(data);
      break;
  }
});

document.domain解决方案

8.1 使用场景

仅适用于主域名相同,子域名不同的情况。

8.2 实现方式

// 在父域名和子域名都设置相同的domain
// 例如:a.example.com和b.example.com
document.domain = 'example.com';

最佳实践与注意事项

9.1 安全性考虑

  1. 始终验证请求来源
  2. 使用白名单限制允许的域名
  3. 合理配置CORS头部
  4. 避免使用Access-Control-Allow-Origin: *
  5. 敏感操作使用HTTPS

9.2 性能优化

  1. 合理设置CORS缓存时间
  2. 减少预检请求
  3. 选择合适的跨域方案
  4. 使用CDN加速