跨域资源共享与安全性实践

176 阅读4分钟

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

浏览器的同源策略(Same-Origin Policy)限制了不同源之间的资源交互,作为Web安全的第一道防线。当协议、域名或端口任一不同时,浏览器将阻止跨域请求,防止恶意网站窃取数据。

// 同源示例
// 当前页面URL: https://example.com/page.html

// ✅ 同源 - 相同协议、域名和端口
fetch('https://example.com/api/data')

// ❌ 非同源 - 不同协议
fetch('http://example.com/api/data')

// ❌ 非同源 - 不同域名
fetch('https://api.example.com/data')

// ❌ 非同源 - 不同端口
fetch('https://example.com:8080/api/data')

CORS:跨域资源共享机制

CORS(Cross-Origin Resource Sharing)是W3C标准,通过HTTP头部字段扩展同源策略,允许服务器声明哪些源可以访问其资源。

简单请求与预检请求

简单请求满足特定条件(如GET、HEAD或特定Content-Type的POST请求)时,浏览器直接发送带有Origin头的请求:

// 客户端发起简单请求
fetch('https://api.example.com/data', {
  method: 'GET',
  headers: {
    'Content-Type': 'text/plain'
  }
});

// 请求头包含
// Origin: https://example.com

服务器响应需包含允许跨域的头信息:

// 服务器响应头
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Credentials: true  // 允许携带凭证

预检请求处理复杂情况,浏览器先发送OPTIONS请求确认服务器是否允许实际请求:

// 客户端发起包含自定义头的请求
fetch('https://api.example.com/data', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Custom-Header': 'value'
  },
  body: JSON.stringify({ key: 'value' })
});

浏览器自动发送预检请求:

OPTIONS /data HTTP/1.1
Host: api.example.com
Origin: https://example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, X-Custom-Header

服务器必须响应允许的方法和头部:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type, X-Custom-Header
Access-Control-Max-Age: 86400  // 预检请求缓存时间(秒)

服务端实现CORS

Node.js (Express)实现

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

// CORS中间件
app.use((req, res, next) => {
  // 允许特定源访问,或使用*允许所有源(不推荐)
  res.header('Access-Control-Allow-Origin', 'https://example.com');
  
  // 允许携带凭证
  res.header('Access-Control-Allow-Credentials', 'true');
  
  // 允许的请求方法
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  
  // 允许的请求头
  res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
  
  // 预检请求缓存时间
  res.header('Access-Control-Max-Age', '86400');
  
  // 预检请求直接返回成功
  if (req.method === 'OPTIONS') {
    return res.status(204).end();
  }
  
  next();
});

// 或使用cors包
// const cors = require('cors');
// app.use(cors({
//   origin: 'https://example.com',
//   credentials: true,
//   methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
//   allowedHeaders: ['Origin', 'X-Requested-With', 'Content-Type', 'Accept', 'Authorization']
// }));

app.get('/api/data', (req, res) => {
  res.json({ message: '这是跨域数据' });
});

app.listen(3000, () => {
  console.log('服务器运行在 http://localhost:3000');
});

JSONP:历史遗留的跨域方案

在CORS出现前,JSONP利用<script>标签不受同源策略限制的特性实现跨域:

// 客户端JSONP实现
function jsonp(url, callback) {
  const script = document.createElement('script');
  const callbackName = 'jsonp_' + Math.random().toString(36).substr(2, 5);
  
  // 将回调函数挂载到全局
  window[callbackName] = function(data) {
    callback(data);
    document.body.removeChild(script);
    delete window[callbackName];
  };
  
  // 添加回调参数
  script.src = `${url}${url.includes('?') ? '&' : '?'}callback=${callbackName}`;
  document.body.appendChild(script);
}

// 使用JSONP获取数据
jsonp('https://api.example.com/data', function(data) {
  console.log('获取的数据:', data);
});

服务端实现:

// Node.js JSONP响应
app.get('/api/data', (req, res) => {
  const data = { message: '这是JSONP返回的数据' };
  const callback = req.query.callback || 'callback';
  
  // 返回可执行的JavaScript代码
  res.type('text/javascript');
  res.send(`${callback}(${JSON.stringify(data)})`);
});

JSONP与CORS对比

特性JSONPCORS
支持的请求方法仅GET所有HTTP方法
错误处理有限完善
安全性较低,容易受XSS攻击较高,有完整控制机制
实现复杂度中等
现代应用推荐度不推荐,仅作兼容强烈推荐

跨域安全风险与防御策略

CSRF攻击与防御

跨站请求伪造(CSRF)利用用户已认证身份发起恶意请求:

<!-- 恶意网站的钓鱼链接 -->
<img src="https://bank.example.com/transfer?to=attacker&amount=1000" style="display:none">

防御策略:

  1. CSRF令牌:每个表单包含服务器生成的唯一令牌
// 前端实现
fetch('https://api.example.com/transfer', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
  },
  credentials: 'include',
  body: JSON.stringify({ to: 'friend', amount: 100 })
});

// 服务端验证
function verifyCSRFToken(req, res, next) {
  const token = req.headers['x-csrf-token'];
  if (!token || !validateToken(token, req.session.csrfToken)) {
    return res.status(403).json({ error: 'CSRF验证失败' });
  }
  next();
}
  1. SameSite Cookie:限制第三方网站使用Cookie
Set-Cookie: sessionId=abc123; SameSite=Strict; Secure; HttpOnly
  1. 验证Referer/Origin:检查请求来源
function checkReferer(req, res, next) {
  const referer = req.headers.referer || req.headers.referrer;
  if (!referer || !referer.startsWith('https://example.com')) {
    return res.status(403).json({ error: '非法请求来源' });
  }
  next();
}

XSS与内容安全策略

对抗跨站脚本(XSS)攻击,通过内容安全策略(CSP)限制资源加载:

<!-- 在HTML中设置CSP -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' https://trusted.cdn.com; img-src 'self' https://trusted-images.com">

服务器设置:

Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.cdn.com; connect-src 'self' https://api.example.com

生产环境中的最佳实践

合理配置CORS

  1. 精确控制允许的源:避免使用*通配符
// 动态控制允许的源
const allowedOrigins = ['https://example.com', 'https://admin.example.com'];

app.use((req, res, next) => {
  const origin = req.headers.origin;
  if (allowedOrigins.includes(origin)) {
    res.header('Access-Control-Allow-Origin', origin);
  }
  // ...其他CORS配置
  next();
});
  1. 合理设置预检缓存时间:减少OPTIONS请求频率

  2. 优先使用子域而非完全不同域:简化CORS配置

代理服务器模式

通过同源服务器代理请求,完全规避跨域限制:

// 前端配置(Vue CLI)
// vue.config.js
module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: 'https://api.example.com',
        changeOrigin: true,
        pathRewrite: {
          '^/api': ''
        }
      }
    }
  }
}

// 前端请求
fetch('/api/data')  // 实际请求 https://api.example.com/data

Node.js后端代理:

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

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

app.listen(3000);

边缘情况处理

  1. 复杂自定义请求头:确保预检响应正确配置

  2. 上传文件的跨域处理:文件上传常需特殊配置

// 前端文件上传
const formData = new FormData();
formData.append('file', document.querySelector('#fileInput').files[0]);

fetch('https://api.example.com/upload', {
  method: 'POST',
  credentials: 'include',
  body: formData
});

// 服务端处理
// 确保响应包含正确的CORS头
app.post('/upload', (req, res) => {
  // ...文件处理逻辑
  res.header('Access-Control-Allow-Origin', 'https://example.com');
  res.header('Access-Control-Allow-Credentials', 'true');
  res.json({ success: true });
});
  1. WebSocket跨域:WebSocket也需遵循CORS
// WebSocket客户端
const socket = new WebSocket('wss://api.example.com/ws');

// WebSocket服务器(Node.js)
const WebSocket = require('ws');
const http = require('http');
const server = http.createServer();

const wss = new WebSocket.Server({ server });

wss.on('connection', function connection(ws, req) {
  // 验证Origin
  const origin = req.headers.origin;
  if (!['https://example.com', 'https://admin.example.com'].includes(origin)) {
    ws.close();
    return;
  }
  
  ws.on('message', function message(data) {
    console.log('received: %s', data);
  });
});

server.listen(8080);

实战案例:单页应用与API交互

下面展示React应用调用不同源API的完整解决方案:

// React组件
import { useState, useEffect } from 'react';

function CrossOriginDemo() {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // 获取CSRF令牌
    const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
    
    async function fetchData() {
      try {
        const response = await fetch('https://api.example.com/data', {
          method: 'GET',
          headers: {
            'Content-Type': 'application/json',
            'X-CSRF-Token': csrfToken
          },
          credentials: 'include' // 发送跨域Cookie
        });
        
        if (!response.ok) {
          throw new Error(`HTTP error! Status: ${response.status}`);
        }
        
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err.message);
        console.error('获取数据失败:', err);
      } finally {
        setLoading(false);
      }
    }
    
    fetchData();
  }, []);
  
  // 数据提交
  const handleSubmit = async (event) => {
    event.preventDefault();
    const formData = new FormData(event.target);
    const payload = Object.fromEntries(formData.entries());
    
    try {
      const response = await fetch('https://api.example.com/submit', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
        },
        credentials: 'include',
        body: JSON.stringify(payload)
      });
      
      if (!response.ok) {
        throw new Error(`提交失败: ${response.status}`);
      }
      
      const result = await response.json();
      alert('提交成功!');
    } catch (err) {
      alert(`错误: ${err.message}`);
    }
  };
  
  if (loading) return <div>加载中...</div>;
  if (error) return <div>错误: {error}</div>;
  
  return (
    <div className="cross-origin-demo">
      <h2>跨域数据获取演示</h2>
      <div className="data-display">
        <pre>{JSON.stringify(data, null, 2)}</pre>
      </div>
      
      <form onSubmit={handleSubmit}>
        <h3>提交数据</h3>
        <div>
          <label>
            名称:
            <input type="text" name="name" required />
          </label>
        </div>
        <div>
          <label>
            消息:
            <textarea name="message" required></textarea>
          </label>
        </div>
        <button type="submit">提交</button>
      </form>
    </div>
  );
}

export default CrossOriginDemo;

未来发展与浏览器趋势

随着Web平台安全要求提高,跨域安全机制不断演进:

  1. Fetch Metadata请求头:提供更多请求背景信息

    Sec-Fetch-Site: cross-site
    Sec-Fetch-Mode: cors
    Sec-Fetch-Dest: image
    
  2. 隐私沙盒(Privacy Sandbox):第三方Cookie逐步淘汰后的替代方案

  3. Cross-Origin-Opener-Policy (COOP)和Cross-Origin-Embedder-Policy (COEP):增强跨域隔离

总结

跨域资源共享是现代Web应用不可避免的挑战。合理配置CORS、代理服务器和安全防御措施,能有效平衡跨域需求与安全风险。前端工程师需全面理解这些机制,确保应用既可互通数据,又能抵御恶意攻击。随着技术演进,持续关注跨域安全最佳实践,将成为开发高质量Web应用的关键能力。

参考资源

官方文档和规范

  1. MDN Web Docs - 跨源资源共享(CORS)
  2. W3C Cross-Origin Resource Sharing规范
  3. MDN Web Docs - 同源策略
  4. MDN Web Docs - 内容安全策略(CSP)
  5. Fetch标准规范
  6. Cookie规范 - SameSite属性

开发者指南和教程

  1. Google Web Fundamentals - CORS指南
  2. web.dev - SameSite Cookie解释
  3. Authentication & Authorization 认证与授权最佳实践
  4. OWASP跨站请求伪造(CSRF)防御备忘单
  5. OWASP跨站脚本(XSS)防御备忘单
  6. Fetch Metadata请求头说明

框架和库文档

  1. Express CORS中间件文档
  2. Axios拦截器和CORS配置
  3. React中处理CORS问题的最佳实践
  4. ws: Node.js WebSocket库
  5. http-proxy-middleware文档

博客和社区文章

  1. 深入了解CORS - Jakub Marian的博客
  2. HTTP Cookie安全性的历史演变 - Troy Hunt的博客
  3. 前端安全最佳实践 - Snyk博客

如果你觉得这篇文章有帮助,欢迎点赞收藏,也期待在评论区看到你的想法和建议!👇

终身学习,共同成长。

咱们下一期见

💻