跨域问题 cors

34 阅读8分钟

一、 一切的根源:同源策略 (Same-Origin Policy)

在谈论 CORS 之前,我们必须先理解它的“母亲”——同源策略 (SOP)

这是一个由浏览器强制执行的、至关重要的安全模型。它规定:一个源(origin)的文档或脚本,不能与另一个源的资源进行交互。

什么是“源”?一个源由 协议(protocol)、域名(domain)、端口(port) 三者共同定义。只要有一个不同,就是不同的源。

URL源 (Origin)http://www.example.com 是否同源?
http://www.example.com/index.htmlhttp://www.example.com
https://www.example.comhttps://www.example.com否 (协议不同)
http://api.example.comhttp://api.example.com否 (域名不同)
http://www.example.com:8080http://www.example.com:8080否 (端口不同)

SOP 的目的: 想象一下,如果你在浏览器的一个标签页中登录了你的网上银行,然后在另一个标签页中打开了一个恶意网站。如果没有同源策略,那个恶意网站的脚本就可以轻易地向你的银行网站发送请求,读取你的账户信息、甚至进行转账操作,后果不堪设想。SOP 就是为了防止这种“跨站请求伪造”(CSRF)等攻击而存在的。

但是,随着 Web 应用越来越复杂,前后端分离成为主流,前端应用(如 http://www.myapp.com)需要请求后端 API(如 http://api.myapp.com)是家常便饭。这明显违反了同源策略。怎么办?

CORS (Cross-Origin Resource Sharing) 闪亮登场!


二、 CORS 是什么?

CORS 是一种机制,它使用额外的 HTTP 头来告诉浏览器,允许运行在一个源上的 Web 应用访问位于另一源上的特定资源。

核心思想: 跨源请求是否安全,不应该由浏览器“一刀切”地决定,而应该由**被请求的资源所在的服务器**来决定。

所以,请记住一个关键点:CORS 的整个流程是由浏览器自动触发和强制执行的,但配置和决定权在后端服务器。 前端开发者通常只需要正常发送请求即可(除了个别情况,后面会讲)。

CORS 将跨源请求分为两类:简单请求非简单请求(通常需要预检)


三、 简单请求 (Simple Request)

浏览器认为某些请求“风险较小”,符合特定条件的,就划为“简单请求”。它不会触发“预检”(Preflight)请求,而是直接发送。

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

  1. 请求方法 (Method) 是以下三者之一:

    • GET
    • HEAD
    • POST
  2. HTTP 头信息 (Headers) 不超出以下几种字段:

    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type(但值仅限于 application/x-www-form-urlencodedmultipart/form-datatext/plain 这三种)
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width
    • Width

简单请求的具体过程:

假设我们的前端页面源是 http://www.a.com,要请求 http://api.b.com/users

  1. 浏览器端(自动行为):

    • 浏览器发现这是一个跨源请求,并且判断它是一个“简单请求”。
    • 在发送请求时,浏览器会自动在 HTTP 请求头中添加一个 Origin 字段,表明请求来自哪个源。
    GET /users HTTP/1.1
    Host: api.b.com
    Origin: http://www.a.com
    Accept: */*
    
  2. 服务器端(需要配置):

    • 服务器收到请求,看到 Origin 字段,知道了这是一个来自 http://www.a.com 的跨源请求。
    • 服务器根据自己的 CORS 配置策略,判断是否允许这个源的请求。
    • 如果允许,服务器在返回的响应头中,必须包含一个 Access-Control-Allow-Origin 字段,其值可以是请求的源 http://www.a.com,或者是 * (表示接受任意源的请求)。
    HTTP/1.1 200 OK
    Content-Type: application/json
    Access-Control-Allow-Origin: http://www.a.com
    
    {"data": [...]}
    
    • 如果不允许,服务器可以不返回这个头,或者返回一个错误的源。
  3. 浏览器端(再次自动行为):

    • 浏览器收到响应后,检查响应头中是否存在 Access-Control-Allow-Origin 字段。
    • 如果存在,且其值包含了当前的源 (http://www.a.com) 或为 *,浏览器就认为请求成功,将响应数据交给 JavaScript 回调函数处理。
    • 如果不存在,或者值不匹配,浏览器就会拦截这个响应,认为请求失败。你在控制台会看到经典的 CORS 错误,即使服务器已经成功处理了请求并返回了 200 状态码,前端的 JavaScript 代码也拿不到任何响应数据。

四、 非简单请求 / 复杂请求 (Preflighted Request)

只要不满足上述“简单请求”条件的任何一条,都会被视为“非简单请求”。例如:

  • 使用了 PUT, DELETE, PATCH 等方法。
  • Content-Type 的值是 application/json
  • 请求头中包含了自定义的头部字段,如 X-Token

对于这些“可能对服务器数据产生副作用”的请求,浏览器出于安全考虑,在发送真实请求之前,会先发送一个“预检请求”(Preflight Request)去“探探路”,询问服务器是否允许即将到来的真实请求。

预检请求的具体过程(两步走):

第 1 步:预检请求 (Preflight Request)

这是一个 OPTIONS 方法的请求,由浏览器自动发起。

  1. 浏览器端(自动行为):

    • 浏览器准备发送一个 PUT 请求到 http://api.b.com/users/1,并带有 Content-Type: application/json 和自定义头 X-Token
    • 浏览器判断这是一个非简单请求,于是先发送一个 OPTIONS 预检请求。
    • 这个预检请求包含了几个关键的头部字段:
      • Origin: 表明请求来源。
      • Access-Control-Request-Method: 告知服务器,接下来的真实请求会使用什么 HTTP 方法(例如 PUT)。
      • Access-Control-Request-Headers: 告知服务器,接下来的真实请求会带有哪些自定义的头部字段(例如 Content-TypeX-Token)。
    OPTIONS /users/1 HTTP/1.1
    Host: api.b.com
    Origin: http://www.a.com
    Access-Control-Request-Method: PUT
    Access-Control-Request-Headers: Content-Type, X-Token
    
  2. 服务器端(需要配置):

    • 服务器收到这个 OPTIONS 请求,解析出预检信息。
    • 服务器检查并判断是否允许来自 http://www.a.com 的、使用 PUT 方法的、并且带有 Content-TypeX-Token 头部的请求。
    • 如果允许,服务器返回一个 200 OK204 No Content 的响应,并且响应头中必须包含以下几个关键字段来“授权”:
      • Access-Control-Allow-Origin: 必须的,允许的源。
      • Access-Control-Allow-Methods: 允许的请求方法列表,如 GET, POST, PUT
      • Access-Control-Allow-Headers: 允许的请求头列表,如 Content-Type, X-Token
      • Access-Control-Max-Age: (可选) 预检请求的有效时间(秒)。在此期间,浏览器无需为同样的请求再次发送预检请求,可以直接缓存结果。
    HTTP/1.1 204 No Content
    Access-Control-Allow-Origin: http://www.a.com
    Access-Control-Allow-Methods: GET, POST, PUT, DELETE
    Access-Control-Allow-Headers: Content-Type, X-Token
    Access-Control-Max-Age: 86400
    
  3. 浏览器端(再次自动行为):

    • 浏览器收到预检响应,检查这些 Access-Control-* 头是否满足即将发送的真实请求。
    • 如果预检通过,浏览器才会继续发送第 2 步的真实请求。
    • 如果预检失败(比如服务器没有返回相应的 Allow 头),浏览器会直接报错,真实请求根本不会被发送出去。

第 2 步:真实请求 (Actual Request)

预检成功后,浏览器发送的真实请求过程就和简单请求一模一样了。

  1. 浏览器端: 发送真实的 PUT 请求,同样会自动带上 Origin 头。
    PUT /users/1 HTTP/1.1
    Host: api.b.com
    Origin: http://www.a.com
    Content-Type: application/json
    X-Token: some-auth-token
    
    {"name": "New Name"}
    
  2. 服务器端: 收到真实请求,处理业务逻辑,并在响应头中再次带上 Access-Control-Allow-Origin注意:这一步不能少!
  3. 浏览器端: 收到响应,检查 Access-Control-Allow-Origin,如果匹配,则请求成功,数据交给 JS 处理。

五、 附带身份凭证 (Credentials) 的请求

默认情况下,跨源请求不会发送 Cookies、HTTP认证信息或客户端SSL证书等身份凭证。

如果需要发送,必须满足前后端双方的共同约定

  1. 前端设置:

    • 在使用 fetch 时,设置 credentials: 'include'
    • 在使用 XMLHttpRequest 时,设置 xhr.withCredentials = true
  2. 后端设置:

    • 服务器必须在响应头中返回 Access-Control-Allow-Credentials: true
    • 极其重要的一点: 当服务器返回 Access-Control-Allow-Credentials: true 时,Access-Control-Allow-Origin 的值不能是通配符 *,必须是明确的、与请求头 Origin 值一致的源。这是出于安全考虑。

总结

特性简单请求非简单请求 (预检)
触发条件方法为 GET/HEAD/POST,且头部简单不满足简单请求条件的任何请求,如 PUT/DELETEapplication/json、自定义头等
请求次数1 次2 次 (1次 OPTIONS 预检 + 1次真实请求)
浏览器行为直接发送请求,请求头自动加 Origin先发 OPTIONS 预检请求,带 Request-Method/Headers,通过后再发真实请求
服务器配置响应头需包含 Access-Control-Allow-Origin预检响应需包含 Allow-Origin/Methods/Headers,真实响应也需包含 Allow-Origin
核心目的验证来源是否被允许在执行可能修改数据的操作前,先确认服务器是否允许该操作

希望这份超详细的讲解能让你对 CORS 有一个全面而深刻的理解!