理解跨域资源共享(CORS):原理、配置与解决方案

CORS:浏览器里的“防火墙”,还是好心邻居?

想象一下,你家住在一栋公寓楼里,你和邻居们共享一片公共区域。平时大家互相串个门,借个工具啥的,都没问题。但如果有一天,隔壁老王突然想把你家保险柜里的钱直接搬走,你肯定不乐意,物业也会跳出来阻止。

浏览器和网站之间的关系,有点像你和邻居。CORS(Cross-Origin Resource Sharing,跨域资源共享)就像是浏览器这栋“公寓楼”里的“物业”,它负责管理不同“住户”(网站)之间的资源访问。

1. 什么是“域”?为什么会有“跨域”?

要理解CORS,首先得搞清楚“域”的概念。简单来说,一个“域”由三部分组成:

  • 协议 (Protocol): 比如 httphttps,就像你家的门铃是电铃还是声控的。
  • 域名 (Domain): 比如 example.com,就像你住的楼号。
  • 端口 (Port): 比如 80443,就像你家的房间号。

只有当这三部分完全一致,才算同一个域。

举个例子:

  • http://www.example.comhttps://www.example.com不同域(协议不同)
  • http://www.example.comhttp://api.example.com不同域(域名不同)
  • http://www.example.comhttp://www.example.com:8080不同域(端口不同)
  • http://www.example.comhttp://www.example.com相同域

好了,现在我们知道什么是“域”了。所谓“跨域”,就是指一个网页的 JavaScript 代码试图向与当前网页不同域的服务器发起请求。

为什么浏览器要搞出这么个“跨域”的概念呢?

这就要说到安全问题了。如果没有跨域限制,恶意网站就可以轻松地冒充用户,窃取用户的敏感信息,比如 Cookie、localStorage 里的数据等等。想象一下,你登录了银行网站,一个恶意网站偷偷地向银行服务器发起请求,把你的账号里的钱转走了,是不是很可怕?

所以,浏览器出于安全考虑,默认禁止跨域请求。这就像公寓楼的物业,默认情况下禁止隔壁老王随意进入你家。

2. CORS 的工作原理:浏览器和服务器之间的“暗号”

CORS 并不是完全禁止跨域请求,而是提供了一种机制,允许服务器告诉浏览器,哪些域的请求是被允许的。

CORS 的核心在于,浏览器在发起跨域请求时,会在请求头里添加一个 Origin 字段,这个字段包含了发起请求的页面的域。

服务器收到请求后,会根据 Origin 字段,判断是否允许这个请求。如果允许,服务器会在响应头里添加一些特殊的 CORS 相关的字段,告诉浏览器这个请求是被允许的。

如果浏览器发现响应头里没有包含允许的 CORS 相关的字段,就会阻止 JavaScript 代码获取响应结果。

这整个过程,就像浏览器和服务器之间在“对暗号”,浏览器先问:“我是来自http://www.example.com的,我能访问你的资源吗?”服务器回答:“可以,http://www.example.com是被允许的。”如果服务器说:“不行,http://www.example.com不在我的白名单里。”浏览器就会直接拒绝。

3. CORS 的两种请求:简单请求和预检请求

CORS 请求分为两种类型:简单请求(Simple Request)和预检请求(Preflight Request)。

3.1 简单请求

简单请求满足以下所有条件:

  • 请求方法是 GETHEADPOST
  • 除了浏览器自动设置的请求头之外,只包含以下几种:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type (但仅限于 application/x-www-form-urlencoded, multipart/form-data, 或 text/plain)
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width
    • Width
  • 请求中的 ReadableStream 对象未被使用。

对于简单请求,浏览器会直接发起请求,并在请求头中添加 Origin 字段。服务器根据 Origin 字段判断是否允许请求,并在响应头中添加 CORS 相关的字段。

例如,一个简单请求的请求头可能如下所示:

Origin: http://www.example.com

服务器的响应头可能如下所示:

Access-Control-Allow-Origin: http://www.example.com

Access-Control-Allow-Origin 字段指定了允许访问资源的域。如果值为 *,表示允许所有域访问。

3.2 预检请求

如果请求不满足简单请求的条件,浏览器会先发起一个“预检请求”(Preflight Request)。预检请求使用 OPTIONS 方法,用于询问服务器是否允许真正的请求。

预检请求的请求头中包含以下字段:

  • Origin: 发起请求的域。
  • Access-Control-Request-Method: 实际请求使用的 HTTP 方法。
  • Access-Control-Request-Headers: 实际请求中包含的自定义请求头。

服务器收到预检请求后,会根据请求头中的信息,判断是否允许真正的请求。如果允许,服务器会在响应头中添加以下字段:

  • Access-Control-Allow-Origin: 允许访问资源的域。
  • Access-Control-Allow-Methods: 允许使用的 HTTP 方法。
  • Access-Control-Allow-Headers: 允许使用的自定义请求头。
  • Access-Control-Max-Age: 预检请求的缓存时间,单位是秒。

浏览器收到预检请求的响应后,会根据响应头中的信息,判断是否允许发起真正的请求。如果允许,浏览器才会发起真正的请求。

例如,一个预检请求的请求头可能如下所示:

OPTIONS /api/data HTTP/1.1
Origin: http://www.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization

服务器的响应头可能如下所示:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://www.example.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

4. 解决 CORS 问题的几种方法

既然 CORS 这么重要,那么,当我们遇到 CORS 问题时,应该如何解决呢?

4.1 服务器端配置 CORS

这是最推荐的解决方案。通过在服务器端设置 CORS 相关的响应头,可以灵活地控制哪些域可以访问你的资源。

不同的服务器端语言和框架,配置 CORS 的方式略有不同。

  • Node.js (Express): 可以使用 cors 中间件。

    const express = require('express');
    const cors = require('cors');
    const app = express();
    
    app.use(cors()); // 允许所有域访问
    
    // 或者,指定允许的域
    // app.use(cors({
    //   origin: 'http://www.example.com'
    // }));
    
    app.get('/api/data', (req, res) => {
      res.json({ message: 'Hello from the API!' });
    });
    
    app.listen(3000, () => {
      console.log('Server listening on port 3000');
    });
  • Java (Spring Boot): 可以使用 @CrossOrigin 注解。

    import org.springframework.web.bind.annotation.CrossOrigin;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    @CrossOrigin(origins = "http://www.example.com") // 允许 http://www.example.com 访问
    public class MyController {
    
        @GetMapping("/api/data")
        public String getData() {
            return "Hello from the API!";
        }
    }
  • Python (Flask): 可以使用 Flask-CORS 扩展。

    from flask import Flask
    from flask_cors import CORS
    
    app = Flask(__name__)
    CORS(app) # 允许所有域访问
    
    # 或者,指定允许的域
    # CORS(app, origins="http://www.example.com")
    
    @app.route("/api/data")
    def hello():
        return "Hello from the API!"
    
    if __name__ == '__main__':
        app.run(debug=True)

在配置 CORS 时,需要注意以下几点:

  • Access-Control-Allow-Origin 的值可以是具体的域名,也可以是 *。如果是 *,表示允许所有域访问,但需要注意安全风险。
  • 如果需要允许携带 Cookie 的跨域请求,需要设置 Access-Control-Allow-Credentialstrue,并且 Access-Control-Allow-Origin 的值不能为 *,必须是具体的域名。
  • 对于预检请求,需要正确设置 Access-Control-Allow-MethodsAccess-Control-Allow-Headers,否则浏览器会拒绝发起真正的请求。

4.2 使用 JSONP

JSONP (JSON with Padding) 是一种古老的跨域解决方案。它的原理是利用 <script> 标签可以跨域请求的特性,将数据包裹在一个回调函数中,然后通过 <script> 标签加载这个数据。

JSONP 的优点是兼容性好,可以支持老版本的浏览器。缺点是只能发起 GET 请求,并且存在安全风险。

4.3 使用代理服务器

可以搭建一个代理服务器,让前端代码向代理服务器发起请求,然后代理服务器再向目标服务器发起请求。由于前端代码和代理服务器是同域的,所以不会触发 CORS 限制。

这种方法的优点是可以解决复杂的 CORS 问题,缺点是需要搭建和维护代理服务器。

4.4 修改 Hosts 文件(仅用于本地开发)

在本地开发时,可以通过修改 Hosts 文件,将不同的域名指向同一个 IP 地址,从而绕过 CORS 限制。

这种方法只适用于本地开发环境,不能用于生产环境。

5. CORS 的一些“坑”

CORS 虽然解决了跨域问题,但也带来了一些“坑”。

  • 预检请求的开销: 对于非简单请求,每次请求都会先发起一个预检请求,这会增加请求的延迟。
  • Cookie 的问题: 如果需要携带 Cookie 的跨域请求,需要进行额外的配置,否则 Cookie 可能无法传递。
  • 安全性问题: 如果 Access-Control-Allow-Origin 设置为 *,可能会带来安全风险,因为任何域都可以访问你的资源。

6. 总结:CORS,一个需要理解和尊重的“规则”

CORS 是浏览器为了安全而设置的一道“防火墙”,但它并不是完全禁止跨域请求,而是提供了一种机制,让服务器可以控制哪些域可以访问自己的资源。

理解 CORS 的原理,掌握解决 CORS 问题的几种方法,可以帮助我们更好地开发 Web 应用,避免踩坑。

记住,CORS 不是一个bug,而是一个特性。我们需要理解它,尊重它,并合理地利用它。就像你和邻居之间需要互相尊重,才能和谐共处一样。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注