跨域资源共享(CORS)完全指南:从基础概念到实际应用

前言

在现代Web开发中,跨域资源共享(CORS)是一个非常重要但又常常让人困惑的概念。无论是前端开发者还是后端开发者,都需要深入理解CORS的工作原理,以便正确处理跨域请求。本文将基于《JavaScript高级程序设计(第三版)》第21.4节的内容,并结合现代Web开发实践,全面解析CORS的机制、配置和常见问题解决方案。

一、什么是跨域资源共享(CORS)?

跨源资源共享(CORS,Cross-Origin Resource Sharing)是一种基于HTTP头的机制,该机制通过允许服务器标示除了它自己以外的其他源(域、协议或端口),使得浏览器允许这些源访问加载自己的资源。

简单来说,CORS是一种安全机制,它允许一个域上的Web应用访问另一个域上的资源。这解决了浏览器同源策略的限制,使得现代Web应用可以安全地进行跨域数据交互。

1.1 同源策略与跨域请求

出于安全性考虑,浏览器实施了同源策略(Same-Origin Policy),限制了脚本内发起的跨源HTTP请求。这意味着使用XMLHttpRequest和Fetch API的Web应用程序只能从加载应用程序的同一个域请求HTTP资源,除非响应报文包含了正确的CORS响应头。

同源的定义是:协议、域名、端口都相同。以下是一些示例:

  • https://example.comhttps://example.com - 同源
  • https://example.comhttps://api.example.com - 不同源(子域名不同)
  • https://example.comhttp://example.com - 不同源(协议不同)
  • https://example.comhttps://example.com:8080 - 不同源(端口不同)

1.2 CORS的作用

CORS机制允许Web应用服务器进行跨源访问控制,从而使跨源数据传输得以安全进行。现代浏览器支持在API容器中(例如XMLHttpRequest或Fetch)使用CORS,以降低跨源HTTP请求所带来的风险。

二、CORS的工作原理

CORS的工作机制可以分为两种类型:简单请求和预检请求。

2.1 简单请求

某些请求不会触发CORS预检请求,这类请求被称为简单请求。满足以下条件的请求被视为简单请求:

  1. 使用以下HTTP方法之一:

    • GET
    • HEAD
    • POST
  2. 除了被用户代理自动设置的标头字段外,只允许人为设置以下标头:

    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type(但仅限于以下三种值)
      • text/plain
      • multipart/form-data
      • application/x-www-form-urlencoded
    • Range(只允许简单的范围标头值)
  3. 请求中没有使用ReadableStream对象。

我们通过一个示例来说明简单请求的工作流程:

javascript 复制代码
// 客户端代码
const fetchPromise = fetch("https://api.example.com/data");

fetchPromise
  .then((response) => response.json())
  .then((data) => {
    console.log(data);
  });

这个请求会发送以下HTTP请求:

makefile 复制代码
GET /data HTTP/1.1
Host: api.example.com
User-Agent: Mozilla/5.0 ...
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Origin: https://example.com
Connection: keep-alive

服务器响应:

yaml 复制代码
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 00:23:53 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://example.com
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: application/json

{"message": "Hello from API"}

2.2 预检请求

与简单请求不同,"需预检的请求"要求必须首先使用OPTIONS方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。

以下情况会触发预检请求:

  1. 使用GET、HEAD、POST以外的HTTP方法
  2. 使用了自定义的HTTP头
  3. Content-Type为application/json等非简单类型

示例代码:

javascript 复制代码
// 客户端代码
const fetchPromise = fetch("https://api.example.com/data", {
  method: "POST",
  mode: "cors",
  headers: {
    "Content-Type": "application/json",
    "X-Custom-Header": "value",
  },
  body: JSON.stringify({ name: "John", age: 30 }),
});

fetchPromise.then((response) => {
  console.log(response.status);
});

CORS请求的完整流程如下图所示:

三、CORS相关的HTTP头

CORS机制通过一系列HTTP头来实现跨域访问控制。下面我们详细介绍这些HTTP头。

3.1 请求头

Origin

Origin请求头表明了请求的来源(协议、主机、端口)。

arduino 复制代码
Origin: https://example.com

Access-Control-Request-Method

在预检请求中,浏览器使用此头来告知服务器实际请求将使用哪种HTTP方法。

makefile 复制代码
Access-Control-Request-Method: POST

Access-Control-Request-Headers

在预检请求中,浏览器使用此头来告知服务器实际请求将携带哪些HTTP头。

go 复制代码
Access-Control-Request-Headers: content-type,x-custom-header

3.2 响应头

Access-Control-Allow-Origin

这是最重要的CORS响应头,它指定了哪些源可以访问资源。

允许所有源访问:

makefile 复制代码
Access-Control-Allow-Origin: *

只允许特定源访问:

arduino 复制代码
Access-Control-Allow-Origin: https://example.com

Access-Control-Allow-Methods

指定允许访问资源的HTTP方法。

sql 复制代码
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS

Access-Control-Allow-Headers

指定允许在实际请求中使用的HTTP头。

makefile 复制代码
Access-Control-Allow-Headers: Content-Type, X-Custom-Header

Access-Control-Allow-Credentials

指示是否允许携带凭据(如cookies、TLS客户端证书等)。

yaml 复制代码
Access-Control-Allow-Credentials: true

注意:当使用凭据时,Access-Control-Allow-Origin不能设置为"*",必须指定具体的源。

Access-Control-Expose-Headers

允许浏览器访问的响应头列表。

makefile 复制代码
Access-Control-Expose-Headers: X-Custom-Header, X-Another-Header

Access-Control-Max-Age

指定预检请求的结果可以被缓存多久(以秒为单位)。

makefile 复制代码
Access-Control-Max-Age: 86400

四、服务器端配置示例

4.1 Node.js/Express配置

javascript 复制代码
const express = require('express');
const app = express();

// 添加CORS中间件
app.use((req, res, next) => {
  // 允许所有源访问(开发环境)
  res.header('Access-Control-Allow-Origin', '*');
  
  // 或者只允许特定源访问(生产环境)
  // res.header('Access-Control-Allow-Origin', 'https://example.com');
  
  // 允许的HTTP方法
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  
  // 允许的HTTP头
  res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization, X-Custom-Header');
  
  // 允许携带凭据
  res.header('Access-Control-Allow-Credentials', 'true');
  
  // 预检请求缓存时间
  res.header('Access-Control-Max-Age', '86400');
  
  // 处理预检请求
  if (req.method === 'OPTIONS') {
    res.sendStatus(200);
  } else {
    next();
  }
});

app.get('/api/data', (req, res) => {
  res.json({ message: 'Hello from API' });
});

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

4.2 Nginx配置

nginx 复制代码
server {
    listen 80;
    server_name api.example.com;
    
    location / {
        # 允许所有源访问(开发环境)
        add_header 'Access-Control-Allow-Origin' '*';
        
        # 或者只允许特定源访问(生产环境)
        # add_header 'Access-Control-Allow-Origin' 'https://example.com';
        
        # 允许的HTTP方法
        add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
        
        # 允许的HTTP头
        add_header 'Access-Control-Allow-Headers' 'Origin, X-Requested-With, Content-Type, Accept, Authorization, X-Custom-Header';
        
        # 允许携带凭据
        add_header 'Access-Control-Allow-Credentials' 'true';
        
        # 预检请求缓存时间
        add_header 'Access-Control-Max-Age' '86400';
        
        # 处理预检请求
        if ($request_method = 'OPTIONS') {
            return 204;
        }
        
        # 其他配置...
    }
}

4.3 Apache配置

apache 复制代码
<IfModule mod_headers.c>
    # 允许所有源访问(开发环境)
    Header set Access-Control-Allow-Origin "*"
    
    # 或者只允许特定源访问(生产环境)
    # Header set Access-Control-Allow-Origin "https://example.com"
    
    # 允许的HTTP方法
    Header set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
    
    # 允许的HTTP头
    Header set Access-Control-Allow-Headers "Origin, X-Requested-With, Content-Type, Accept, Authorization, X-Custom-Header"
    
    # 允许携带凭据
    Header set Access-Control-Allow-Credentials "true"
    
    # 预检请求缓存时间
    Header set Access-Control-Max-Age "86400"
    
    # 处理预检请求
    <If "%{REQUEST_METHOD} == 'OPTIONS'">
        Header set Content-Length 0
        Header set Content-Type text/plain
        Redirect 204 /
    </If>
</IfModule>

五、客户端使用示例

5.1 使用Fetch API

javascript 复制代码
// 简单GET请求
fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('Error:', error));

// POST请求携带自定义头
fetch('https://api.example.com/data', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Custom-Header': 'value'
  },
  body: JSON.stringify({ name: 'John' })
})
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('Error:', error));

// 携带凭据的请求
fetch('https://api.example.com/data', {
  method: 'GET',
  credentials: 'include' // 包含cookies
})
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('Error:', error));

5.2 使用XMLHttpRequest

javascript 复制代码
// 创建XMLHttpRequest对象
const xhr = new XMLHttpRequest();

// 配置请求
xhr.open('GET', 'https://api.example.com/data', true);

// 设置携带凭据
xhr.withCredentials = true;

// 设置请求头
xhr.setRequestHeader('X-Custom-Header', 'value');

// 处理响应
xhr.onreadystatechange = function() {
  if (xhr.readyState === 4) {
    if (xhr.status === 200) {
      const data = JSON.parse(xhr.responseText);
      console.log(data);
    } else {
      console.error('Error:', xhr.status);
    }
  }
};

// 发送请求
xhr.send();

六、常见问题与解决方案

6.1 CORS错误排查

当遇到CORS错误时,浏览器控制台通常会显示类似以下的错误信息:

csharp 复制代码
Access to fetch at 'https://api.example.com/data' from origin 'https://example.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

排查步骤:

  1. 检查请求的Origin头是否正确
  2. 检查服务器响应是否包含正确的Access-Control-Allow-Origin头
  3. 对于预检请求,检查服务器是否正确响应OPTIONS请求
  4. 检查是否需要携带凭据,如果需要,确保Access-Control-Allow-Origin不是通配符

6.2 凭据与通配符问题

当请求需要携带凭据(cookies、HTTP认证信息)时,服务器不能将Access-Control-Allow-Origin设置为"*"。

错误示例:

http 复制代码
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

正确示例:

http 复制代码
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Credentials: true

6.3 预检请求失败

预检请求失败通常是因为服务器没有正确处理OPTIONS请求,或者没有返回正确的CORS响应头。

解决方案:

  1. 确保服务器正确处理OPTIONS请求
  2. 确保返回正确的Access-Control-Allow-Methods和Access-Control-Allow-Headers头
  3. 设置适当的Access-Control-Max-Age以减少预检请求频率

6.4 缓存问题

CORS响应头可以被缓存,但需要注意以下几点:

  1. 如果服务器使用通配符"*"而不是指定具体的源,需要在Vary响应头中包含Origin:

    http 复制代码
    Access-Control-Allow-Origin: *
    Vary: Origin
  2. 预检请求的结果可以通过Access-Control-Max-Age头进行缓存

七、安全考虑

7.1 避免过度宽松的CORS策略

虽然在开发环境中使用通配符"*"很方便,但在生产环境中应该始终指定具体的源:

javascript 复制代码
// 不安全的做法
res.header('Access-Control-Allow-Origin', '*');

// 安全的做法
res.header('Access-Control-Allow-Origin', 'https://trusted-domain.com');

7.2 验证Origin头

在服务器端,可以验证Origin头以确保只允许受信任的源访问:

javascript 复制代码
const allowedOrigins = [
  'https://example.com',
  'https://app.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();
});

7.3 限制HTTP方法和头

只允许必要的HTTP方法和头,避免暴露不必要的接口:

http 复制代码
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: Content-Type, Authorization

八、最佳实践

8.1 开发环境与生产环境的区别

在开发环境中,可以使用宽松的CORS策略以方便调试:

javascript 复制代码
// 开发环境
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*');
  // 其他CORS设置...
  next();
});

在生产环境中,应该使用严格的CORS策略:

javascript 复制代码
// 生产环境
const allowedOrigins = [
  'https://yourdomain.com',
  'https://app.yourdomain.com'
];

app.use((req, res, next) => {
  const origin = req.headers.origin;
  if (allowedOrigins.includes(origin)) {
    res.header('Access-Control-Allow-Origin', origin);
  }
  // 其他CORS设置...
  next();
});

8.2 使用CORS中间件

对于Node.js应用,可以使用专门的CORS中间件:

javascript 复制代码
const cors = require('cors');

const corsOptions = {
  origin: ['https://example.com', 'https://app.example.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
  maxAge: 86400
};

app.use(cors(corsOptions));

8.3 监控和日志

记录CORS相关的请求和错误,有助于及时发现问题:

javascript 复制代码
app.use((req, res, next) => {
  // 记录CORS请求
  if (req.headers.origin) {
    console.log(`CORS request from ${req.headers.origin} to ${req.url}`);
  }
  
  // 处理CORS
  // ...
  
  next();
});

总结

CORS是现代Web开发中不可或缺的一部分,理解其工作原理对于构建安全、高效的Web应用至关重要。通过正确配置CORS策略,我们可以在保证安全的前提下实现跨域资源共享。

关键要点回顾:

  1. CORS通过HTTP头实现跨域访问控制
  2. 简单请求和预检请求有不同的处理机制
  3. 正确设置CORS响应头是解决跨域问题的关键
  4. 生产环境中应使用严格的CORS策略
  5. 注意凭据与通配符的使用限制
  6. 合理利用预检请求缓存提高性能

通过本文的介绍,希望你能够更好地理解和应用CORS,解决实际开发中的跨域问题。

最后,创作不易请允许我插播一则自己开发的"数规规-数字助手"(有各种预测分析)小程序广告,感兴趣可以微信小程序体验放松放松,程序员也要有点娱乐生活,搞不好就中个排列五了呢?

感兴趣可以搜索微信小程序"数规规数字助手"体验体验!!

相关推荐
等风起8813 小时前
Element Plus实现TreeSelect树形选择在不同父节点下子节点有相同id的双向绑定联动
前端·javascript
小胖霞3 小时前
阿里云域名解析 + Nginx 反向代理 + HTTPS 全流程:从 IP 访问到加密域名的完整配置
前端
2301_801252223 小时前
Vue中的指令
前端·javascript·vue.js
烛阴3 小时前
彻底搞懂Lua闭包
前端·lua
天***88963 小时前
Chrome扩展安装插件教程,Edge安装插件扩展教程,浏览器安装扩展程序方法
前端·chrome·edge
心.c4 小时前
深拷贝浅拷贝
开发语言·前端·javascript·ecmascript
IT_陈寒4 小时前
Vue 3.4性能优化实战:5个鲜为人知的Composition API技巧让打包体积减少40%
前端·人工智能·后端
前端九哥4 小时前
💻【急招!27届前端实习生】广州4399实习太幸福了!江景+三餐+健身房全都有😭
前端·面试·招聘
咖啡の猫5 小时前
Vue全局事件总线
前端·javascript·vue.js