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

JavaScript中的跨域资源共享(CORS)解决方案

2024-01-101.4k 阅读

一、理解跨域问题

1.1 同源策略

在深入探讨跨域资源共享(CORS)解决方案之前,我们首先要理解什么是跨域。这一切都源于浏览器的同源策略(Same - Origin Policy)。同源策略是一种安全机制,它限制了从一个源加载的文档或脚本如何与另一个源的资源进行交互。所谓“同源”,指的是协议、域名和端口号都相同。例如:

  • http://example.com
  • http://example.com:8080(端口不同,非同 源)
  • https://example.com(协议不同,非同源)
  • http://sub.example.com(域名不同,非同源)

当一个网页尝试从一个非同源的服务器获取资源时,浏览器会根据同源策略阻止这种操作。这是为了防止恶意网站通过脚本获取其他网站的敏感信息,比如用户在其他网站上的登录状态、个人数据等。

1.2 跨域场景

在实际开发中,跨域场景经常出现。例如,当我们的前端应用部署在一个域名下,而后端 API 服务部署在另一个域名或者不同端口上时,就会产生跨域问题。常见的场景有:

  • 前后端分离项目:前端代码部署在 http://frontend.example.com,后端 API 部署在 http://api.example.com
  • 使用第三方 API:比如在自己的网站上调用 Google Maps API,自己的网站可能是 http://mywebsite.com,而 Google Maps API 的域名是 https://maps.googleapis.com

二、跨域资源共享(CORS)概述

2.1 CORS 是什么

跨域资源共享(CORS)是一种机制,它使用额外的 HTTP 头来告诉浏览器,让运行在一个源上的 Web 应用被准许访问来自不同源的指定的资源。当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,资源会发起一个跨域 HTTP 请求。

CORS 背后的基本思想,就是使用自定义的 HTTP 头来让浏览器与服务器进行沟通,从而决定是否允许跨域请求。

2.2 CORS 的优势

与其他解决跨域问题的方法(如 JSONP)相比,CORS 具有以下优势:

  • 支持多种请求方法:CORS 支持所有的 HTTP 请求方法,如 GET、POST、PUT、DELETE 等,而 JSONP 仅支持 GET 请求。
  • 更安全:由于 CORS 是基于 HTTP 头的机制,它可以在服务器端进行更细粒度的控制,相比 JSONP 只能通过回调函数传递数据,安全性更高。
  • 符合标准:CORS 是 W3C 标准,得到了现代浏览器的广泛支持,兼容性好。

三、CORS 相关的 HTTP 头

3.1 Access - Control - Allow - Origin

这是 CORS 中最重要的 HTTP 头之一。它指定了允许跨域请求的源。可以设置为具体的源,如 http://allowed - origin.com,也可以设置为 *,表示允许所有源的请求。例如,在服务器端(以 Node.js 为例)设置该头:

const http = require('http');
const server = http.createServer((req, res) => {
    res.setHeader('Access - Control - Allow - Origin', '*');
    res.end('Hello, CORS!');
});
server.listen(3000, () => {
    console.log('Server running on port 3000');
});

但需要注意的是,当请求中包含 Credentials(如 withCredentials: true 在前端设置)时,不能使用 *,必须指定具体的源。

3.2 Access - Control - Allow - Methods

该头指定了服务器允许的跨域请求的方法。常见的值有 GETPOSTPUTDELETE 等。多个方法可以用逗号分隔。例如:

res.setHeader('Access - Control - Allow - Methods', 'GET, POST, PUT, DELETE');

3.3 Access - Control - Allow - Headers

当请求中包含自定义的 HTTP 头时,服务器需要通过 Access - Control - Allow - Headers 头来指定允许的自定义头。比如前端发送请求时设置了 X - Custom - Header

fetch('http://api.example.com/data', {
    method: 'POST',
    headers: {
        'Content - Type': 'application/json',
        'X - Custom - Header': 'Some value'
    },
    body: JSON.stringify({ data: 'example' })
});

服务器端就需要设置:

res.setHeader('Access - Control - Allow - Headers', 'Content - Type, X - Custom - Header');

3.4 Access - Control - Max - Age

这个头指定了预检请求(preflight request)的结果能够被缓存的最长时间,单位为秒。预检请求是 CORS 机制中,对于某些跨域请求(如非简单请求)在正式请求之前发送的一个探测性请求,用来检查服务器是否允许该实际请求。例如设置预检请求结果缓存 1728000 秒(20 天):

res.setHeader('Access - Control - Max - Age', '1728000');

3.5 Access - Control - Expose - Headers

默认情况下,浏览器只允许 JavaScript 访问响应中的有限几个标准头,如 Cache - ControlContent - LanguageContent - Type 等。如果希望前端能够访问其他自定义头,就需要通过 Access - Control - Expose - Headers 来指定。例如:

res.setHeader('Access - Control - Expose - Headers', 'X - Custom - Response - Header');

然后在前端可以这样访问:

fetch('http://api.example.com/data')
   .then(response => {
        const customHeader = response.headers.get('X - Custom - Response - Header');
        console.log(customHeader);
        return response.json();
    })
   .then(data => {
        console.log(data);
    });

3.6 Origin

这是前端请求中携带的头,它表示请求来自哪个源。服务器可以根据这个头的值来决定是否允许跨域请求。例如在 Node.js 中获取该头:

const http = require('http');
const server = http.createServer((req, res) => {
    const origin = req.headers.origin;
    // 根据 origin 进行判断和设置 CORS 相关头
    res.end('Hello, CORS!');
});
server.listen(3000, () => {
    console.log('Server running on port 3000');
});

四、简单请求与非简单请求

4.1 简单请求

根据 CORS 规范,满足以下条件的请求被认为是简单请求:

  • 请求方法:是以下之一:GETHEADPOST
  • HTTP 头:只包含以下几个头:AcceptAccept - LanguageContent - LanguageContent - Type,并且 Content - Type 的值仅限于以下几种:application/x - www - form - urlencodedmultipart/form - datatext/plain

简单请求不会触发预检请求,浏览器会直接发出请求。服务器端只需在响应中设置合适的 CORS 头,就可以处理跨域问题。例如,一个简单的 GET 请求:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF - 8">
    <meta name="viewport" content="width=device - width, initial - scale = 1.0">
    <title>Simple CORS Request</title>
</head>

<body>
    <script>
        fetch('http://api.example.com/data')
           .then(response => response.json())
           .then(data => {
                console.log(data);
            });
    </script>
</body>

</html>

服务器端(Node.js):

const http = require('http');
const server = http.createServer((req, res) => {
    res.setHeader('Access - Control - Allow - Origin', '*');
    res.setHeader('Content - Type', 'application/json');
    const data = { message: 'Hello from server' };
    res.end(JSON.stringify(data));
});
server.listen(3000, () => {
    console.log('Server running on port 3000');
});

4.2 非简单请求

不满足简单请求条件的请求就是非简单请求。例如,使用 PUT 方法或者设置了非标准的 Content - Typeapplication/json 且请求方法不是 GETHEADPOST 的请求。非简单请求在正式请求之前,浏览器会先发送一个预检请求(OPTIONS 方法)。

预检请求的目的是检查服务器是否允许该实际请求。服务器需要正确处理预检请求,返回合适的 CORS 相关头。例如,一个使用 PUT 方法且 Content - Typeapplication/json 的非简单请求:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF - 8">
    <meta name="viewport" content="width=device - width, initial - scale = 1.0">
    <title>Non - Simple CORS Request</title>
</head>

<body>
    <script>
        const data = { key: 'value' };
        fetch('http://api.example.com/data', {
            method: 'PUT',
            headers: {
                'Content - Type': 'application/json'
            },
            body: JSON.stringify(data)
        })
           .then(response => response.json())
           .then(data => {
                console.log(data);
            });
    </script>
</body>

</html>

服务器端(Node.js)处理预检请求:

const http = require('http');
const server = http.createServer((req, res) => {
    if (req.method === 'OPTIONS') {
        res.setHeader('Access - Control - Allow - Origin', '*');
        res.setHeader('Access - Control - Allow - Methods', 'PUT');
        res.setHeader('Access - Control - Allow - Headers', 'Content - Type');
        res.end();
    } else if (req.method === 'PUT') {
        let body = '';
        req.on('data', chunk => {
            body += chunk.toString();
        });
        req.on('end', () => {
            const data = JSON.parse(body);
            res.setHeader('Access - Control - Allow - Origin', '*');
            res.setHeader('Content - Type', 'application/json');
            const responseData = { message: 'Data updated successfully', data };
            res.end(JSON.stringify(responseData));
        });
    }
});
server.listen(3000, () => {
    console.log('Server running on port 3000');
});

五、Credentials(凭证)与 CORS

5.1 什么是 Credentials

在跨域请求中,Credentials 指的是用户认证相关的信息,如 cookies、HTTP 认证头(如 Authorization 头)等。默认情况下,浏览器在跨域请求时不会发送这些凭证信息。

5.2 发送 Credentials

如果需要在跨域请求中发送凭证,前端需要在请求中设置 withCredentials: true。例如:

fetch('http://api.example.com/data', {
    method: 'POST',
    headers: {
        'Content - Type': 'application/json'
    },
    body: JSON.stringify({ data: 'example' }),
    credentials: 'include'
})
   .then(response => response.json())
   .then(data => {
        console.log(data);
    });

同时,服务器端的 Access - Control - Allow - Origin 头不能设置为 *,必须设置为具体的允许的源。并且需要设置 Access - Control - Allow - Credentials: true。例如在 Node.js 中:

const http = require('http');
const server = http.createServer((req, res) => {
    res.setHeader('Access - Control - Allow - Origin', 'http://frontend.example.com');
    res.setHeader('Access - Control - Allow - Credentials', 'true');
    // 处理请求逻辑
    res.end('Hello, CORS with credentials!');
});
server.listen(3000, () => {
    console.log('Server running on port 3000');
});

5.3 注意事项

  • 同源策略限制:即使设置了 withCredentials: true,浏览器仍然遵循同源策略。例如,不能通过跨域请求读取其他域的 cookies,只能发送自己域下的 cookies 到允许的跨域服务器。
  • 不同浏览器行为:在一些较旧的浏览器中,对 withCredentials 的支持可能存在问题,需要进行兼容性测试。

六、CORS 在不同环境中的实现

6.1 Node.js 中实现 CORS

在 Node.js 中,可以使用多种方式实现 CORS。一种简单的方法是手动设置 CORS 相关头,如前面示例中所示。另外,也可以使用 cors 中间件。首先安装 cors

npm install cors

然后在 Express 应用中使用:

const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors());
app.get('/data', (req, res) => {
    const data = { message: 'Hello from server' };
    res.json(data);
});
const port = 3000;
app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

cors 中间件有很多可配置选项,比如可以限制允许的源:

app.use(cors({
    origin: 'http://allowed - origin.com'
}));

6.2 Java 中实现 CORS

在 Java 的 Spring Boot 项目中,可以通过配置 WebMvcConfigurer 来实现 CORS。创建一个配置类:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsConfig {

    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**")
                       .allowedOrigins("*")
                       .allowedMethods("GET", "POST", "PUT", "DELETE")
                       .allowedHeaders("*");
            }
        };
    }
}

这里 addMapping("/**") 表示对所有的请求路径都应用 CORS 配置,allowedOrigins("*") 允许所有源,allowedMethods 定义了允许的请求方法,allowedHeaders("*") 允许所有头。

6.3 Python(Flask)中实现 CORS

在 Flask 应用中,可以使用 flask - cors 扩展来实现 CORS。首先安装:

pip install flask - cors

然后在 Flask 应用中使用:

from flask import Flask
from flask_cors import CORS

app = Flask(__name__)
CORS(app)

@app.route('/data')
def get_data():
    return {'message': 'Hello from server'}

也可以对 CORS 进行更详细的配置,比如限制源:

CORS(app, origins=['http://allowed - origin.com'])

七、CORS 常见问题与解决方法

7.1 预检请求失败

预检请求失败通常是因为服务器没有正确设置 CORS 相关头。检查 Access - Control - Allow - MethodsAccess - Control - Allow - Headers 等头是否设置正确,是否与前端请求匹配。例如,如果前端请求使用了 PUT 方法且设置了自定义头 X - Custom - Header,服务器端需要在预检请求的响应中设置:

res.setHeader('Access - Control - Allow - Methods', 'PUT');
res.setHeader('Access - Control - Allow - Headers', 'X - Custom - Header');

7.2 凭证问题

如果在跨域请求中发送凭证(withCredentials: true)时出现问题,首先确保服务器端的 Access - Control - Allow - Origin 设置为具体的源,而不是 *,并且设置了 Access - Control - Allow - Credentials: true。同时,检查前端请求是否正确设置了 credentials: 'include'

7.3 浏览器兼容性问题

虽然 CORS 得到了现代浏览器的广泛支持,但在一些较旧的浏览器中可能存在兼容性问题。可以通过 feature detection(特性检测)来处理,例如在使用 fetch 发送跨域请求时,可以先检查 fetch 是否支持 credentials 选项:

if ('fetch' in window && 'credentials' in new Request('')) {
    fetch('http://api.example.com/data', {
        method: 'POST',
        headers: {
            'Content - Type': 'application/json'
        },
        body: JSON.stringify({ data: 'example' }),
        credentials: 'include'
    })
       .then(response => response.json())
       .then(data => {
            console.log(data);
        });
} else {
    // 处理不支持的情况,例如使用其他方式进行跨域请求
}

八、CORS 与安全

8.1 CORS 安全风险

虽然 CORS 是一种安全机制,但如果配置不当,也会带来安全风险。例如,如果将 Access - Control - Allow - Origin 设置为 * 且允许发送凭证,可能会导致恶意网站通过跨域请求获取用户的敏感信息。另外,如果服务器对 Access - Control - Allow - Headers 设置不当,允许了过多不必要的自定义头,也可能被攻击者利用。

8.2 安全配置建议

  • 限制允许的源:尽量避免使用 *,而是指定具体的允许的源。例如,如果前端应用部署在 http://frontend.example.com,服务器端的 Access - Control - Allow - Origin 应设置为 http://frontend.example.com
  • 严格控制允许的方法和头:只允许实际需要的 HTTP 方法和自定义头。例如,如果应用只使用 GETPOST 方法,Access - Control - Allow - Methods 就只设置为 GET, POST
  • 定期审查 CORS 配置:随着应用的发展和架构的变化,定期审查 CORS 配置,确保其仍然安全和符合业务需求。

通过深入理解 CORS 的原理、相关 HTTP 头、不同类型的请求以及在不同环境中的实现,开发者可以有效地解决 JavaScript 中的跨域问题,并确保应用的安全性。同时,注意常见问题的排查和安全配置,能够让跨域资源共享在安全的前提下顺利进行。