浏览器同源策略
跨域(CORS)是由浏览器的同源策略(Same-Origin Policy, SOP)引起的,它是浏览器端的安全机制,与前后端服务无关。
同源(Same-Origin) = 协议 + 域名 + 端口必须相同,否则会触发跨域。
请求来源 |
目标地址 |
是否同源 |
http://example.com:8080 |
http://example.com:8080 |
✅ 同源 |
http://example.com:8080 |
http://example.com:9090 |
❌ 跨域(端口不同) |
http://example.com |
https://example.com |
❌ 跨域(协议不同) |
http://example.com |
http://api.example.com |
❌ 跨域(域名不同) |
跨源资源共享CORS
跨源资源共享(CORS,或通俗地译为跨域资源共享)是一种基于 HTTP 头的机制(用于解决浏览器同源策略问题),该机制通过允许服务器标示除了它自己以外的其他源(域、协议或端口),使得浏览器允许这些源访问加载自己的资源。跨源资源共享还通过一种机制来检查服务器是否会允许要发送的真实请求,该机制通过浏览器发起一个到服务器托管的跨源资源的“预检”请求。在预检中,浏览器发送的头中标示有 HTTP 方法和真实请求中会用到的头。
CORS 机制允许 Web 应用服务器进行跨源访问控制,从而使跨源数据传输得以安全进行。
结合前后端理解,CORS(Cross-Origin Resource Sharing,跨域资源共享)是服务器(后端)允许浏览器上的非同源网站(前端)访问加载自己的资源。
简单请求
简单请求不会触发 CORS 预检请求,可以直接访问非同源资源。如下图所示:

以下是浏览器发送给服务器的请求报文,满足简单请求的条件:
1 2 3 4 5 6 7 8
| GET /resources/public-data/ HTTP/1.1 Host: bar.other User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip,deflate Connection: keep-alive Origin: https://foo.example
|
请求标头字段 Origin
表明该请求来源于 http://foo.example
。
服务器响应报文:
1 2 3 4 5 6 7 8 9 10 11
| HTTP/1.1 200 OK Date: Mon, 01 Dec 2008 00:23:53 GMT Server: Apache/2 Access-Control-Allow-Origin: * Keep-Alive: timeout=2, max=100 Connection: Keep-Alive Transfer-Encoding: chunked Content-Type: application/xml
[…XML Data…]
|
服务端返回的 Access-Control-Allow-Origin
标头的 Access-Control-Allow-Origin: *
值表明,该资源可以被任意外源访问。
预检请求
与简单请求不同,“需预检的请求”要求必须首先使用 OPTIONS
方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。”预检请求“的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。如下图所示:

如下是一个需要执行预检请求的 HTTP 请求:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| const fetchPromise = fetch("https://bar.other/doc", { method: "POST", mode: "cors", headers: { "Content-Type": "text/xml", "X-PINGOTHER": "pingpong", }, body: "<person><name>Arun</name></person>", });
fetchPromise.then((response) => { console.log(response.status); });
|
上面请求的 Content-Type
为 application/xml
,且包含了一个非标准的 HTTP X-PINGOTHER
请求标头,所以该请求需要首先发起“预检请求”。
下面是服务端和客户端完整的信息交互。首次交互是预检请求/响应:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| OPTIONS /doc HTTP/1.1 Host: bar.other User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip,deflate Connection: keep-alive Origin: https://foo.example Access-Control-Request-Method: POST Access-Control-Request-Headers: X-PINGOTHER, Content-Type
HTTP/1.1 204 No Content Date: Mon, 01 Dec 2008 01:15:39 GMT Server: Apache/2 Access-Control-Allow-Origin: https://foo.example Access-Control-Allow-Methods: POST, GET, OPTIONS Access-Control-Allow-Headers: X-PINGOTHER, Content-Type Access-Control-Max-Age: 86400 Vary: Accept-Encoding, Origin Keep-Alive: timeout=2, max=100 Connection: Keep-Alive
|
客户端的请求标头字段:
Access-Control-Request-Method
告知服务器,实际请求将使用 POST
方法。
Access-Control-Request-Headers
告知服务器,实际请求将携带两个自定义请求标头字段:X-PINGOTHER
与 Content-Type
。
服务端的响应标头字段:
Access-Control-Allow-Origin: https://foo.example
限制请求的源域。
Access-Control-Allow-Methods
表明服务器允许客户端使用 POST
和 GET
方法发起请求。
Access-Control-Allow-Headers
表明服务器允许请求中携带字段 X-PINGOTHER
与 Content-Type
。
Access-Control-Max-Age
给定了该预检请求可供缓存的时间长短,单位为秒,默认值是 5 秒。在有效时间内,浏览器无须为同一请求再次发起预检请求。以上例子中,该响应的有效时间为 86400 秒,也就是 24 小时。请注意,浏览器自身维护了一个最大有效时间,如果该标头字段的值超过了最大有效时间,将不会生效。
预检请求完成之后,发送实际请求:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| POST /doc HTTP/1.1 Host: bar.other User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip,deflate Connection: keep-alive X-PINGOTHER: pingpong Content-Type: text/xml; charset=UTF-8 Referer: https://foo.example/examples/preflightInvocation.html Content-Length: 55 Origin: https://foo.example Pragma: no-cache Cache-Control: no-cache
<person><name>Arun</name></person>
HTTP/1.1 200 OK Date: Mon, 01 Dec 2008 01:15:40 GMT Server: Apache/2 Access-Control-Allow-Origin: https://foo.example Vary: Accept-Encoding, Origin Content-Encoding: gzip Content-Length: 235 Keep-Alive: timeout=2, max=99 Connection: Keep-Alive Content-Type: text/plain
[Some XML payload]
|
解决浏览器同源策略的方法
前端代理、Nginx 代理和后端配置 CORS 都是解决浏览器的同源策略(Cross-Origin Resource Sharing,CORS)问题的常用方法。
后端开启CORS
后端开启 CORS 允许跨源资源请求,返回 Access-Control-Allow-Origin: * 等允许跨域的响应报文。
nginx反向代理
Nginx 通过代理服务器转发请求,让前端请求 Nginx,而不是直接请求后端,从而绕过浏览器的同源策略。
原理:
- 前端请求 Nginx 服务器(
http://frontend.com/api
)。
- Nginx 反向代理,将请求转发到后端服务(
http://backend.com
)。
- 后端返回数据给 Nginx,Nginx 再把数据返回给前端。(注意 Nginx 缓冲和缓存引发的问题)
- 整个过程中,前端请求的始终是
frontend.com
,不会被浏览器判定为跨域。
Nginx 配置示例如下:
1 2 3 4 5 6 7 8 9 10 11
| server { listen 80; server_name frontend.com;
location /api/ { proxy_pass http://backend.com:9999/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }
|
前端代理(开发环境)
Vue 3 的 Vite 或 Vue CLI 开发服务器的代理配置如下所示:
1 2 3 4 5 6 7 8 9 10 11 12
| devServer: { port: 8080, proxy: { '/api': { target: process.env.VUE_APP_BASE_API, ws: true, changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') } } }
|
上述两种前端代理和Nginx 反向代理配置,其核心目的都是让前端请求看起来是从同源发出的,从而避免浏览器的跨域限制,而不是以 CORS 机制解决浏览器的同源策略。
参考
跨源资源共享官方文档:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS