1 CORS 核心
CORS(Cross-Origin Resource Sharing),即跨域资源共享,是目前最主流的跨域方案,它通过服务器返回的特殊 HTTP 头,允许浏览器放行跨域请求。与传统的 JSONP 相比,CORS 具有明显的优势:
-
支持所有 HTTP 方法(GET/POST/PUT/DELETE 等)
-
更安全可控的权限管理
-
完整的错误处理机制
-
支持携带 Cookie 等认证信息
1.1 跨域请求的核心流程
当浏览器发起跨域请求时,会执行一个关键的 "预检" 流程(Preflight):
-
浏览器先发送一个 OPTIONS 方法的预检请求
-
服务器返回包含 CORS 相关头部的响应
-
浏览器根据响应头决定是否允许实际请求
这个过程就像是客户端和服务器之间的一次 "握手",确保双方就跨域访问的权限达成一致。
1.2 CORS 核心 HTTP 头详解
服务器通过以下关键响应头来控制 CORS 权限:
响应头字段 | 作用描述 | 示例 |
---|---|---|
Access-Control-Allow-Origin | 指定允许跨域请求的源(如 | Access-Control-Allow-Origin: https://example.com |
Access-Control-Allow-Methods | 列出服务器支持的跨域请求方法(如 | Access-Control-Allow-Methods: GET, POST, PUT |
Access-Control-Allow-Headers | 声明请求中允许携带的自定义头部字段(如 | Access-Control-Allow-Headers: X-My-Custom-Header, Content-Type |
Access-Control-Max-Age | 设定预检请求结果的缓存时间(单位:秒),避免频繁发送 OPTIONS 请求。 | Access-Control-Max-Age: 86400 |
Access-Control-Allow-Credentials | 若值为 | Access-Control-Allow-Credentials: true |
Access-Control-Expose-Headers | 指定浏览器可以访问的服务器的响应头列表。多个头以逗号分隔。 | Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header |
2 实战案例
2.1 GET 请求跨域
场景说明:
服务端 A:运行在 3000 端口,提供静态页面
服务端 B:运行在 4000 端口,提供 API 接口
2.1.1 服务端A(servera.js)
// 引入 Express 框架 - 这是一个用于构建 Web 应用和 API 的 Node.js 框架
const express = require('express');// 创建一个 Express 应用实例 - 这将作为我们 Web 服务器的基础
const app = express();// 设置静态文件夹 - 所有位于 'public' 目录下的文件将直接作为静态资源提供
// 例如,public/index.html 可以通过 http://localhost:3000/index.html 访问
app.use(express.static('public'));// 定义监听端口 - 服务器将在这个端口上接收客户端的请求
const port = 3000;// 让应用监听在指定端口并在控制台输出信息
// 当服务器成功启动后,会执行回调函数中的代码
app.listen(port, () => {// 在控制台打印服务器运行信息,包含访问地址console.log(`Server is running on http://localhost:${port}`);
});
2.1.2 静态页面A(indext.html)
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>a</title>
</head>
<body><script>// 定义一个立即执行的异步函数 - 这种模式允许我们使用 await 而不需要单独定义函数(async function () {try {// 使用 fetch API 发送 GET 请求到跨域API - 这是现代浏览器提供的原生 AJAX 方法const response = await fetch('http://localhost:4000/users', {// 指定请求方法为 GET - 表示我们正在请求资源method: 'GET',// 指定请求头,表明期望的响应格式为 JSON - 告诉服务器我们希望收到 JSON 格式的数据headers: {'Accept': 'application/json'}// 使用 then 方法将响应对象转换为 JSON 格式 - fetch 的第一阶段返回的是响应元数据,需要解析 body}).then(response => {// 打印响应头信息 - 这有助于调试服务器返回的额外信息for (let [key, value] of response.headers) {console.log(`${key}: ${value}`);}// 特别打印内容类型 - 确认服务器返回的数据格式console.log(response.headers.get('content-type'));// 将响应体解析为 JSON 格式 - 返回的是一个 Promisereturn response.json();})// 打印获取到的响应 - 这里的 response 已经是解析后的 JSON 数据console.log('response ',response);} catch (error) {// 如果有任何错误,打印错误信息 - 捕获网络错误或解析错误console.error('Error:', error);}// 立即执行上述定义的函数})();</script>
</body>
</html>
2.1.3 服务端B(serverb.js)
// 引入 Express 框架 - 用于构建 Web 服务器和 API 的 Node.js 框架
const express = require('express');// 创建一个 Express 应用实例 - 作为服务器的基础
const app = express();// 使用中间件来设置响应头,以处理跨域问题 - 允许不同源的客户端访问此 API
app.use((req, res, next) => {// 允许所有来源的访问 - 生产环境中应限制为特定域名res.header("Access-Control-Allow-Origin", "*"); // 允许接受的请求头 - 告诉浏览器允许客户端在请求中包含这些头res.header("Access-Control-Allow-Headers", "Accept");// 指定对外暴露的响应头 - 允许客户端访问这些非标准响应头res.header('Access-Control-Expose-Headers', 'X-My-Custom-Header'); // 设置自定义的响应头 - 可用于传递额外信息给客户端res.setHeader('X-My-Custom-Header', 'X-My-Custom-Header');// 调用 next 函数,以便将控制权交给下一个中间件 - 确保请求继续被处理next();
});// 定义一个用户列表 - 模拟数据库中的用户数据
const users = [{id: 1, name: '用户1'}
];// 创建一个端点,返回用户列表 - 处理客户端对 /users 路径的 GET 请求
app.get('/users', (req, res) => {// 将用户列表以 JSON 格式返回 - 设置正确的 Content-Type 并发送数据res.json(users);
});// 定义监听端口 - 服务器将在此端口接收客户端请求
const port = 4000;// 让应用监听在指定端口并在控制台输出信息 - 启动服务器
app.listen(port, () => {// 当服务器开始运行时,打印一条消息 - 指示服务器已成功启动console.log(`Server is running on http://localhost:${port}`);
});
2.2 POST 请求跨域
2.2.1 关键配置:Access-Control-Max-Age
在处理复杂请求(如 POST)时,Access-Control-Max-Age
头非常重要,它可以缓存预检请求的结果,减少不必要的 OPTIONS 请求:
//设置预检请求结果缓存1小时(3600秒)
res.header("Access-Control-Max-Age", "3600");
2.2.2 静态页面A(add.html)
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>a</title>
</head>
<body><script>// 定义一个立即执行的异步函数 - 允许使用 await 语法处理 Promise(async function () {try {// 使用 fetch API 发送 POST 请求到指定的 URL - 用于向服务器提交数据const response = await fetch('http://localhost:4000/users', {// 指定请求方法为 POST - 表示我们正在向服务器提交数据method: 'POST',// 指定请求头 - 设置内容类型为 JSON 并期望 JSON 响应headers: {'Content-Type': 'application/json', // 告诉服务器请求体是 JSON 格式'Accept': 'application/json' // 告诉服务器期望 JSON 格式的响应},// 请求体 - 将 JavaScript 对象转换为 JSON 字符串发送body: JSON.stringify({name: '李四'})// 使用 then 方法将响应对象转换为 JSON 格式 - 解析服务器返回的数据}).then(response => response.json())// 打印获取到的响应 - 这里的 response 是服务器返回的 JSON 数据console.log('response ', response);} catch (error) {// 如果有任何错误,打印错误信息 - 捕获网络错误或解析错误console.error('Error:', error);}// 立即执行上述定义的函数})();</script>
</body>
</html>
2.2.3 服务端B(serverb.js)
// 引入 Express 框架 - 用于构建 Web 服务器和 API 的 Node.js 框架
const express = require('express');// 创建一个 Express 应用实例 - 作为服务器的基础
const app = express();// 设置静态文件夹 - 用于提供静态资源(如 HTML、CSS、JS 文件)
app.use(express.static('public'));// 使用中间件来解析请求体中的 JSON 数据 - 使 req.body 可以获取 JSON 格式数据
app.use(express.json());// 使用中间件来解析请求体中的 urlencoded 数据 - 处理表单提交数据
app.use(express.urlencoded({ extended: false }));// 使用中间件来设置响应头,以处理跨域问题 - 允许不同源的客户端访问此 API
app.use((req, res, next) => {// 允许所有来源的访问 - 生产环境中应限制为特定域名res.header("Access-Control-Allow-Origin", "*"); // 允许接受的请求头 - 更新为同时允许 Accept 和 Content-Type 头res.header("Access-Control-Allow-Headers", "Accept,Content-Type");// 指定对外暴露的响应头 - 允许客户端访问这些非标准响应头res.header('Access-Control-Expose-Headers', 'X-My-Custom-Header'); // 设置自定义的响应头 - 可用于传递额外信息给客户端res.setHeader('X-My-Custom-Header', 'X-My-Custom-Header');// 设置预检请求的结果可以被缓存多久 - 减少 OPTIONS 请求的频率res.header("Access-Control-Max-Age", "3600");// 处理预检请求(HTTP OPTIONS) - 对于复杂请求,浏览器会先发 OPTIONS 请求if (req.method === 'OPTIONS') {// 直接返回 200 状态码,结束预检请求处理return res.sendStatus(200);}// 调用 next 函数,以便将控制权交给下一个中间件 - 确保请求继续被处理next();
});// 定义一个用户列表 - 模拟数据库中的用户数据
const users = [{id: 1, name: '张三'}
];// 创建一个端点,返回用户列表 - 处理客户端对 /users 路径的 GET 请求
app.get('/users', (req, res) => {// 将用户列表以 JSON 格式返回 - 设置正确的 Content-Type 并发送数据res.json(users);
});// 创建一个端点,处理添加用户请求 - 处理客户端对 /users 路径的 POST 请求
app.post('/users', (req, res) => {// 从请求体中获取新用户数据const user = req.body;// 为新用户生成唯一 ID(基于当前最后一个用户的 ID)user.id = users[users.length - 1].id + 1;// 将新用户添加到用户列表users.push(user);// 返回更新后的用户列表 - 通常创建成功返回 201 状态码和新资源res.json(users);
});// 定义监听端口 - 服务器将在此端口接收客户端请求
const port = 4000;// 让应用监听在指定端口并在控制台输出信息 - 启动服务器
app.listen(port, () => {// 当服务器开始运行时,打印一条消息 - 指示服务器已成功启动console.log(`Server is running on http://localhost:${port}`);
});
2.3 携带Cookie的跨域请求
关键概念
-
Cookie:服务器存储在客户端的小段数据,用于跟踪用户状态(如登录信息)
-
跨域限制:默认情况下,浏览器不会在跨域请求中携带 Cookie
-
CORS 与 Cookie 的结合:需服务端与客户端同时配置
关键细节
-
服务端必须:
-
设置
Access-Control-Allow-Origin
为具体域名(非*
) -
添加
Access-Control-Allow-Credentials: true
-
使用
res.cookie()
方法设置 Cookie
-
-
客户端必须:
-
在请求中添加
credentials: 'include'
-
-
安全注意事项:
-
建议使用
httpOnly: true
防止 XSS 攻击 -
生产环境中配合 HTTPS 使用,确保 Cookie 加密传输
-
2.3.1 服务端B(serverb.js)
// 引入 Express 框架 - 用于构建 Web 服务器和 API 的 Node.js 框架
const express = require('express');// 引入 cookie-parser 中间件,用于处理 Cookie - 解析请求中的 cookie 数据
const cookieParser = require('cookie-parser');// 创建一个 Express 应用实例 - 作为服务器的基础
const app = express();// 设置静态文件夹 - 用于提供静态资源(如 HTML、CSS、JS 文件)
app.use(express.static('public'));// 使用 cookie-parser 中间件 - 使 req.cookies 可以获取客户端发送的 cookie
app.use(cookieParser());// 使用中间件来解析请求体中的 JSON 数据 - 使 req.body 可以获取 JSON 格式数据
app.use(express.json());// 使用中间件来解析请求体中的 urlencoded 数据 - 处理表单提交数据
app.use(express.urlencoded({ extended: false }));// 使用中间件来设置响应头,以处理跨域问题 - 允许不同源的客户端访问此 API
app.use((req, res, next) => {// 允许所有来源的访问 - 修改为动态允许当前请求的来源(支持带凭证的 CORS)res.header("Access-Control-Allow-Origin", req.headers.origin); // 允许接受的请求头 - 包括 Accept 和 Content-Typeres.header("Access-Control-Allow-Headers", "Accept,Content-Type");// 指定对外暴露的响应头 - 允许客户端访问这些非标准响应头res.header('Access-Control-Expose-Headers', 'X-My-Custom-Header'); // 设置自定义的响应头 - 可用于传递额外信息给客户端res.setHeader('X-My-Custom-Header', 'X-My-Custom-Header');// 设置预检请求的结果可以被缓存多久 - 减少 OPTIONS 请求的频率res.header("Access-Control-Max-Age", "3600");// 允许发送 Cookie - 启用跨域请求中的 cookie 支持res.header("Access-Control-Allow-Credentials", "true");// 处理预检请求(HTTP OPTIONS) - 对于复杂请求,浏览器会先发 OPTIONS 请求if (req.method === 'OPTIONS') {// 直接返回 200 状态码,结束预检请求处理return res.sendStatus(200);}// 调用 next 函数,以便将控制权交给下一个中间件 - 确保请求继续被处理next();
});// 定义一个用户列表 - 模拟数据库中的用户数据
const users = [{id: 1, name: '张三'}
];// 创建一个端点,返回用户列表 - 处理客户端对 /users 路径的 GET 请求
app.get('/users', (req, res) => {// 将用户列表以 JSON 格式返回 - 设置正确的 Content-Type 并发送数据res.json(users);
});// 创建一个端点,处理添加用户请求 - 处理客户端对 /users 路径的 POST 请求
app.post('/users', (req, res) => {// 从请求体中获取新用户数据const user = req.body;// 为新用户生成唯一 ID(基于当前最后一个用户的 ID)user.id = users[users.length - 1].id + 1;// 将新用户添加到用户列表users.push(user);// 返回更新后的用户列表res.json(users);
});// 创建一个路由,用于计数 - 演示 cookie 的使用
app.get('/count', (req, res) => {// 从请求的 Cookies 中获取 "count" 的值,如果不存在,则默认为 0let count = req.cookies.count || 0;// 将计数器的值加 1count++;// 在响应的 Cookies 中设置 "count" 的值// 同时设置最大过期时间为 24 小时(毫秒为单位)// 并设置只能通过 HTTP 访问(防止 XSS 攻击)res.cookie('count', count, { maxAge: 24 * 60 * 60 * 1000, // 24小时后过期httpOnly: true // 仅通过 HTTP 访问,JavaScript 无法读取});// 将计数器的值以 JSON 格式返回给客户端res.json({ count });
});// 定义监听端口 - 服务器将在此端口接收客户端请求
const port = 4000;// 让应用监听在指定端口并在控制台输出信息 - 启动服务器
app.listen(port, () => {// 当服务器开始运行时,打印一条消息 - 指示服务器已成功启动console.log(`Server is running on http://localhost:${port}`);
});
2.3.2 静态页面A(count.html)
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body><!-- 用于显示计数结果的段落 --><p id="notice"></p><script>// 使用 fetch API 发送 GET 请求到计数端点 - 从服务器获取计数并显示async function getCount() {try {// 发送请求到服务器的 /count 端点const response = await fetch('http://localhost:4000/count', {method: 'GET',// 需要包含此行以发送 Cookie - 跨域请求默认不发送凭证credentials: 'include' }).then(response => response.json())// 将服务器返回的计数更新到页面上document.getElementById('notice').innerHTML = `Count: ${response.count}`;} catch (error) {// 捕获并打印请求过程中的任何错误console.error('Error:', error);}}// 页面加载后立即执行获取计数的函数getCount();</script>
</body>
</html>
CORS 相比 JSONP 更安全、更灵活,支持全类型请求和 Cookie 携带,是现代前端项目的首选跨域方案。通过本文的理论讲解和实战案例,相信您已经对 CORS 有了全面的理解。在实际开发中,合理运用 CORS 可以帮助我们构建更灵活、更强大的应用程序。
下一章将介绍 postMessage
方案 ,敬请期待!