MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Node.js Express 中的 CORS 跨域问题解决方案

2022-07-225.6k 阅读

一、CORS 跨域问题概述

在前端开发中,当一个网页从不同的源(协议、域名、端口号中有一个不同)加载资源时,就会遇到跨域问题。这是浏览器出于安全策略的考虑而实施的同源策略(Same - Origin Policy)所导致的。例如,网页 http://localhost:3000 试图从 http://api.example.com:8080 获取数据,这就违反了同源策略,浏览器会阻止这种跨域请求。

CORS(Cross - Origin Resource Sharing)是一种机制,它通过HTTP 头来允许浏览器向不同源的服务器发出 XMLHttpRequest 或 Fetch 请求,从而解决跨域问题。在 Node.js 的 Express 框架中,处理 CORS 跨域问题是一个常见的需求,因为 Express 经常被用于搭建后端 API 服务,而前端应用可能部署在不同的域上。

二、Express 中解决 CORS 问题的常用方法

(一)使用 cors 中间件

  1. 安装 cors 中间件 在使用 cors 中间件之前,需要先安装它。在项目的根目录下执行以下命令:
npm install cors
  1. 基本使用 在 Express 应用中,引入并使用 cors 中间件非常简单。以下是一个基本示例:
const express = require('express');
const cors = require('cors');
const app = express();

app.use(cors());

// 定义路由
app.get('/data', (req, res) => {
    res.json({ message: 'This is some data' });
});

const port = 3001;
app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

在上述代码中,通过 app.use(cors())cors 中间件应用到整个 Express 应用。这意味着所有的路由都将允许跨域请求。

  1. 配置特定选项 cors 中间件提供了许多可配置的选项,以满足不同的跨域需求。例如,只允许特定的源访问:
const express = require('express');
const cors = require('cors');
const app = express();

const whitelist = ['http://localhost:3000', 'http://example.com'];
const corsOptions = {
    origin: function (origin, callback) {
        if (whitelist.indexOf(origin)!== -1 ||!origin) {
            callback(null, true);
        } else {
            callback(new Error('Not allowed by CORS'));
        }
    }
};

app.use(cors(corsOptions));

// 定义路由
app.get('/data', (req, res) => {
    res.json({ message: 'This is some data' });
});

const port = 3001;
app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

在这个例子中,corsOptions 配置了 origin 选项。origin 是一个函数,它接收请求的源(origin 参数),并检查该源是否在 whitelist 中。如果在白名单中或者请求没有 Origin 头(例如来自一些没有同源策略限制的工具),则允许跨域请求;否则,返回错误。

  1. 处理预检请求 对于复杂请求(例如使用 PUTDELETE 方法,或者设置了自定义请求头的请求),浏览器会先发送一个预检请求(OPTIONS 请求),以检查服务器是否允许该实际请求。cors 中间件会自动处理预检请求。不过,有时可能需要自定义处理。例如,设置允许的 HTTP 方法和自定义请求头:
const express = require('express');
const cors = require('cors');
const app = express();

const corsOptions = {
    origin: 'http://localhost:3000',
    optionsSuccessStatus: 200,
    methods: ['GET', 'POST', 'PUT', 'DELETE'],
    allowedHeaders: ['Content - Type', 'Authorization']
};

app.use(cors(corsOptions));

// 定义路由
app.get('/data', (req, res) => {
    res.json({ message: 'This is some data' });
});

const port = 3001;
app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

在上述代码中,methods 选项指定了允许的 HTTP 方法,allowedHeaders 选项指定了允许的自定义请求头。optionsSuccessStatus 选项设置预检请求成功时返回的状态码,默认是 204,这里设置为 200。

(二)手动设置 CORS 相关响应头

除了使用 cors 中间件,也可以手动在 Express 应用中设置 CORS 相关的响应头。虽然这种方法相对繁琐,但能让开发者更精细地控制 CORS 配置。

  1. 设置简单的 CORS 头
const express = require('express');
const app = express();

app.use((req, res, next) => {
    res.setHeader('Access - Control - Allow - Origin', '*');
    res.setHeader('Access - Control - Allow - Methods', 'GET, POST, PUT, DELETE');
    res.setHeader('Access - Control - Allow - Headers', 'Content - Type');
    next();
});

// 定义路由
app.get('/data', (req, res) => {
    res.json({ message: 'This is some data' });
});

const port = 3001;
app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

在这个示例中,通过 res.setHeader 方法分别设置了 Access - Control - Allow - Origin 头允许所有源访问,Access - Control - Allow - Methods 头允许 GETPOSTPUTDELETE 方法,Access - Control - Allow - Headers 头允许 Content - Type 头。

  1. 根据请求动态设置 CORS 头 有时候,需要根据请求的具体情况动态设置 CORS 头。例如,只允许特定的源访问:
const express = require('express');
const app = express();

const whitelist = ['http://localhost:3000', 'http://example.com'];

app.use((req, res, next) => {
    const origin = req.headers.origin;
    if (whitelist.indexOf(origin)!== -1 ||!origin) {
        res.setHeader('Access - Control - Allow - Origin', origin || '*');
    } else {
        return res.status(403).send('Forbidden');
    }
    res.setHeader('Access - Control - Allow - Methods', 'GET, POST, PUT, DELETE');
    res.setHeader('Access - Control - Allow - Headers', 'Content - Type');
    next();
});

// 定义路由
app.get('/data', (req, res) => {
    res.json({ message: 'This is some data' });
});

const port = 3001;
app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

在这个代码中,首先获取请求头中的 Origin 字段,然后检查它是否在 whitelist 中。如果在,则将 Access - Control - Allow - Origin 设置为该源;如果不在且 Origin 存在,则返回 403 禁止访问。如果 Origin 不存在(例如来自一些工具的请求),则设置 Access - Control - Allow - Origin*

三、CORS 跨域问题深入分析

(一)同源策略的原理

同源策略是浏览器的一项安全机制,它确保网页只能与同一源的资源进行交互。一个源由协议(如 httphttps)、域名(如 example.com)和端口号(如 80443)组成。例如,http://example.comhttps://example.com 是不同的源,因为协议不同;http://example.com:8080http://example.com:80 也是不同的源,因为端口号不同。

同源策略主要保护用户免受恶意网站的攻击。例如,如果没有同源策略,一个恶意网站可以轻易地通过 XMLHttpRequest 获取用户在银行网站上的敏感信息,因为它可以向银行网站发起请求并获取响应。

(二)CORS 的工作原理

  1. 简单请求 对于简单请求(满足以下条件:使用 GETPOSTHEAD 方法;没有自定义请求头;Content - Typeapplication/x - www - form - urlencodedmultipart/form - datatext/plain),浏览器会直接发出请求。服务器在响应头中返回 CORS 相关信息,例如 Access - Control - Allow - Origin 头,告诉浏览器该请求是否被允许。如果 Access - Control - Allow - Origin 头的值与请求的源匹配(或者为 *),浏览器就会将响应内容返回给前端应用;否则,浏览器会阻止该响应,前端应用无法获取到数据。

  2. 复杂请求 对于复杂请求,浏览器会先发送一个预检请求(OPTIONS 请求)。这个预检请求的目的是询问服务器是否允许实际的请求。预检请求包含一些关于实际请求的信息,例如请求方法、自定义请求头。服务器接收到预检请求后,检查这些信息,并在响应头中返回是否允许该请求。如果服务器允许,它会在响应头中设置 Access - Control - Allow - OriginAccess - Control - Allow - MethodsAccess - Control - Allow - Headers 等头信息。浏览器接收到预检请求的响应后,根据这些头信息决定是否发送实际请求。如果服务器不允许,浏览器会阻止实际请求的发送。

(三)CORS 与 JSONP 的区别

  1. 原理不同
    • JSONP:JSONP 利用 <script> 标签没有同源策略限制的特点来实现跨域。它通过动态创建一个 <script> 标签,将请求的 URL 作为 src 属性值,服务器返回一个 JavaScript 函数调用,函数的参数就是要返回的数据。前端通过定义这个函数来处理接收到的数据。
    • CORS:CORS 是通过在服务器端设置 HTTP 响应头来允许跨域请求。浏览器根据这些头信息来判断是否允许跨域请求和处理响应。
  2. 适用场景不同
    • JSONP:只支持 GET 方法,适用于简单的跨域数据获取场景,例如获取第三方的天气数据等。由于它基于 <script> 标签,在处理复杂请求(如 POST 请求、带自定义头的请求)时存在局限性。
    • CORS:支持所有的 HTTP 方法,适用于各种复杂的跨域请求场景,是现代前端开发中解决跨域问题的主流方式。
  3. 安全性不同
    • JSONP:由于它是通过动态插入 <script> 标签执行 JavaScript 代码,存在一定的安全风险。如果服务器返回的不是预期的函数调用,而是恶意脚本,可能会导致 XSS(跨站脚本攻击)。
    • CORS:相对更安全,因为它是基于服务器设置的响应头,浏览器严格按照同源策略和 CORS 规范来处理请求和响应,减少了潜在的安全漏洞。

四、实际项目中 CORS 问题的优化与注意事项

(一)优化 CORS 配置

  1. 限制允许的源 在实际项目中,尽量不要使用 * 作为 Access - Control - Allow - Origin 的值,因为这意味着允许所有源访问,存在安全风险。应该明确指定允许的源,例如只允许前端应用所在的域名访问后端 API。这样可以减少潜在的恶意访问。
  2. 合理设置预检请求缓存 对于预检请求,浏览器会根据服务器返回的 Access - Control - Max - Age 头信息来缓存预检请求的结果。合理设置这个值可以提高性能。例如,如果 API 的 CORS 配置不会频繁变动,可以将 Access - Control - Max - Age 设置为一个较大的值(单位为秒),这样在有效期内,相同的预检请求就不会再次发送。
const express = require('express');
const cors = require('cors');
const app = express();

const corsOptions = {
    origin: 'http://localhost:3000',
    optionsSuccessStatus: 200,
    methods: ['GET', 'POST', 'PUT', 'DELETE'],
    allowedHeaders: ['Content - Type', 'Authorization'],
    maxAge: 86400 // 设置预检请求缓存 24 小时
};

app.use(cors(corsOptions));

// 定义路由
app.get('/data', (req, res) => {
    res.json({ message: 'This is some data' });
});

const port = 3001;
app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

在上述代码中,通过 maxAge 选项设置了预检请求缓存时间为 86400 秒(24 小时)。

(二)注意事项

  1. 生产环境安全 在生产环境中,确保 CORS 配置的安全性至关重要。除了限制允许的源,还应该注意处理自定义请求头。如果允许自定义请求头,要确保这些头信息不会被恶意利用。例如,对于 Authorization 头,要严格验证其值的合法性,防止非法的身份验证。
  2. 跨域代理 在一些情况下,可能会使用跨域代理来解决 CORS 问题。例如,前端应用通过一个代理服务器来转发对后端 API 的请求,这样可以避免浏览器的同源策略限制。在使用跨域代理时,要注意代理服务器的配置和安全性。代理服务器应该正确转发请求和响应,并且要防止代理服务器成为新的安全漏洞点。
  3. 不同浏览器兼容性 虽然大多数现代浏览器都支持 CORS,但在实际项目中,仍要注意不同浏览器在处理 CORS 时可能存在的兼容性问题。例如,一些旧版本的浏览器可能对 CORS 的支持不完全,或者在处理预检请求时有不同的行为。在开发过程中,要进行充分的浏览器兼容性测试,确保应用在各种主流浏览器上都能正常工作。

五、CORS 跨域问题与其他技术的结合

(一)CORS 与 HTTPS

随着互联网安全要求的提高,越来越多的网站开始使用 HTTPS 协议。在使用 CORS 时,与 HTTPS 的结合非常重要。如果前端应用和后端 API 都使用 HTTPS,浏览器在处理 CORS 请求时会更加严格。例如,当后端 API 返回 Access - Control - Allow - Origin* 时,在 HTTPS 环境下,一些浏览器可能会阻止该响应,因为这可能存在安全风险。

为了在 HTTPS 环境下正确使用 CORS,后端应该明确指定允许的 HTTPS 源。同时,服务器应该正确配置 HTTPS 证书,确保证书的有效性和安全性。例如:

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

const corsOptions = {
    origin: 'https://frontend.example.com',
    optionsSuccessStatus: 200,
    methods: ['GET', 'POST', 'PUT', 'DELETE'],
    allowedHeaders: ['Content - Type', 'Authorization']
};

app.use(cors(corsOptions));

// 定义路由
app.get('/data', (req, res) => {
    res.json({ message: 'This is some data' });
});

const port = 3001;
app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

在上述代码中,origin 明确设置为 HTTPS 源 https://frontend.example.com,以适应 HTTPS 环境下的 CORS 需求。

(二)CORS 与微服务架构

在微服务架构中,可能存在多个服务,每个服务都有自己的 API。前端应用可能需要从不同的微服务获取数据,这就涉及到多个跨域问题。

一种解决方案是在每个微服务中分别配置 CORS。例如,使用 cors 中间件,根据每个微服务的需求设置允许的源、方法和头信息。另一种方法是在网关层统一处理 CORS。网关作为所有请求的入口,可以对请求进行拦截,检查并设置 CORS 相关的响应头。这样可以简化各个微服务的配置,同时便于统一管理 CORS 策略。

以下是一个在网关层(假设使用 Express 搭建网关)处理 CORS 的示例:

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

const corsOptions = {
    origin: 'http://frontend.example.com',
    optionsSuccessStatus: 200,
    methods: ['GET', 'POST', 'PUT', 'DELETE'],
    allowedHeaders: ['Content - Type', 'Authorization']
};

app.use(cors(corsOptions));

// 模拟转发请求到微服务
app.get('/service1/data', (req, res) => {
    // 实际中会转发到 service1 的 API
    res.json({ message: 'Data from service1' });
});

app.get('/service2/data', (req, res) => {
    // 实际中会转发到 service2 的 API
    res.json({ message: 'Data from service2' });
});

const port = 8080;
app.listen(port, () => {
    console.log(`Gateway running on port ${port}`);
});

在这个示例中,网关应用统一设置了 CORS 配置,前端应用通过网关访问不同微服务的 API 时,都能正确处理跨域问题。

六、CORS 跨域问题的常见错误及解决方法

(一)“No 'Access - Control - Allow - Origin' header is present on the requested resource”错误

  1. 错误原因 这个错误表示服务器没有在响应头中设置 Access - Control - Allow - Origin 头。可能是没有正确配置 CORS 中间件,或者手动设置响应头时遗漏了这一项。
  2. 解决方法 如果使用 cors 中间件,确保正确引入并应用了该中间件,例如:
const express = require('express');
const cors = require('cors');
const app = express();

app.use(cors());

// 定义路由
app.get('/data', (req, res) => {
    res.json({ message: 'This is some data' });
});

const port = 3001;
app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

如果手动设置响应头,确保添加了 Access - Control - Allow - Origin 头,例如:

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

app.use((req, res, next) => {
    res.setHeader('Access - Control - Allow - Origin', '*');
    res.setHeader('Access - Control - Allow - Methods', 'GET, POST, PUT, DELETE');
    res.setHeader('Access - Control - Allow - Headers', 'Content - Type');
    next();
});

// 定义路由
app.get('/data', (req, res) => {
    res.json({ message: 'This is some data' });
});

const port = 3001;
app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

(二)“Access to XMLHttpRequest at '...' from origin '...' has been blocked by CORS policy: The 'Access - Control - Allow - Origin' header contains multiple values '...', but only one is allowed”错误

  1. 错误原因 这个错误表示 Access - Control - Allow - Origin 头设置了多个值,而浏览器只允许一个值。通常是在代码中错误地多次设置了 Access - Control - Allow - Origin 头,或者在不同的中间件或路由处理中设置了不一致的值。
  2. 解决方法 检查代码中设置 Access - Control - Allow - Origin 头的部分,确保只设置一次且值正确。例如,如果使用 cors 中间件,不要在其他地方手动再设置 Access - Control - Allow - Origin 头。如果手动设置,确保在整个应用中设置逻辑的一致性。

(三)预检请求失败错误

  1. 错误原因 预检请求失败可能有多种原因,例如服务器返回的 Access - Control - Allow - Methods 头不包含实际请求的方法,或者 Access - Control - Allow - Headers 头不包含实际请求的自定义头。
  2. 解决方法 检查服务器的 CORS 配置,确保 Access - Control - Allow - MethodsAccess - Control - Allow - Headers 头设置正确。例如,如果实际请求使用了 PUT 方法,确保 Access - Control - Allow - Methods 头包含 PUT
const express = require('express');
const cors = require('cors');
const app = express();

const corsOptions = {
    origin: 'http://localhost:3000',
    optionsSuccessStatus: 200,
    methods: ['GET', 'POST', 'PUT', 'DELETE'],
    allowedHeaders: ['Content - Type', 'Authorization']
};

app.use(cors(corsOptions));

// 定义路由
app.put('/data', (req, res) => {
    res.json({ message: 'Data updated' });
});

const port = 3001;
app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

在上述代码中,methods 选项包含了 PUT 方法,以确保预检请求能正确通过。

七、总结 CORS 跨域问题解决方案在 Express 中的应用

在 Node.js 的 Express 框架中,解决 CORS 跨域问题有多种方法,最常用的是使用 cors 中间件和手动设置 CORS 相关响应头。使用 cors 中间件简单方便,能满足大多数常见的跨域需求,并且提供了丰富的可配置选项。手动设置响应头则可以让开发者更精细地控制 CORS 配置,但需要更多的代码编写和注意设置的准确性。

在实际项目中,要根据项目的具体需求和安全要求来选择合适的解决方案。同时,要注意优化 CORS 配置,避免安全风险,并且关注不同技术结合时(如与 HTTPS、微服务架构)的 CORS 处理。遇到常见错误时,要能准确分析原因并及时解决。通过合理应用 CORS 跨域问题解决方案,能够确保前端应用与 Express 后端 API 之间的顺畅交互,为用户提供更好的体验。