文章目录
- 一、Origin 含义
- 二、跨源资源共享:**Cross-Origin Resource Sharing** CORS
- 2.1 跨域的定义
- 2.2 功能概述
- 2.3 场景示例
- 2.3.1 简单请求
- 2.3.2 Preflighted requests:预检请求
- 2.4 header
- 2.4.1 http request header
- 2.4.1.1 Origin
- 2.4.1.2 Access-Control-Request-Method
- 2.4.1.3 Access-Control-Request-Headers
- 2.4.2 http response header
- 2.4.2.1 Access-Control-Allow-Origin
- 2.4.2.2 Access-Control-Expose-Headers
- 2.4.2.3 Access-Control-Max-Age
- 2.4.2.4 Access-Control-Allow-Credentials
- 2.5 解决方案
- 2.5.1 nginx 反向代理
- 2.5.2 预检请求 + 后端添加 response header 解决跨域
http header Origin、跨域、同源
一、Origin 含义
origin 是来源,原始的意思
The Origin
request header indicates the origin (scheme, hostname, and port) that caused the request. For example, if a user agent needs to request resources included in a page, or fetched by scripts that it executes, then the origin of the page may be included in the request.
http request header 的 Origin 字段,的内容是 scheme,hostname,port 字段。代表请求的来源。
例如,如果一个 client,需要请求一个网页资源或脚本,request header 会带有自己的信息。
格式:
Origin: null
Origin: <scheme>://<hostname>
Origin: <scheme>://<hostname>:<port>
格式:
- null:origin 是隐私或者不透明的
- scheme:协议,通常是 http 或 https
- hostname:域名或 ip
- port:(可选的)端口号,数字
描述:
Origin header 和 Referer header 很像,但不透露路径,且可能为空。其表示了来源信息,除非是隐私或不必要的。
一般而言,用户代理将原始请求报头添加到:
- cross-origin requests(跨域请求)。
- same-origin request(同源请求),除了 GET、HEAD 的方法(即 POST、OPTIONS、PUT、PATCH、DELETE)。
以下情况,值可能为 null:
- 不是
http
,https
,ftp
,ws
,wss
, orgopher
(includingblob
,file
anddata
) 的 scheme。 - 跨源图像和媒体数据,包括中的、和元素。
示例:
Origin: https://developer.mozilla.org
Origin: http://developer.mozilla.org:80
参考:https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin
二、跨源资源共享:Cross-Origin Resource Sharing CORS
参考:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS
2.1 跨域的定义
一个协议的 url,由 协议、域名、端口三部分组成。当一个请求 url 的协议、域名、端口三者的任意一个,与当前页面 url 不同即为跨域。
例如:
当前页面,被请求页面,是否跨域,原因
http://www.testlocation.com/,http://www.testlocation.com/index.html,否,同源(协议、域名、端口号均相同)
http://www.testlocation.com/,https://www.testlocation.com/index.html,跨域,协议不同(http/https)
http://www.testlocation.com/, http://www.baidu.com/, 跨域,主域名不同(testlocation/baidu)
http://www.testlocation.com/, http://blog.testlocation.com/, 跨域,子域名不同(www/blog)
http://www.testlocation.com:8080/, http://www.testlocation.com:7001/, 跨域(8080/7001)
跨域的原因:
因为浏览器的同源策略(Same Origin Policy),它是一种约定,是浏览器最核心也是最基本的安全功能,它会组织一个域的 js 脚本和另一个域的内容进行交互,如果缺少了同源策略,浏览器很容易受到 XSS、CSFR 等攻击。
非同源会出现的限制:
无法读取非同源网页的 cookie、localstorage 等
无法接触非同源网页的 DOM 和 js 对象
无法向非同源地址发送 ajax 请求
概念:
跨源资源共享(CORS,或通俗地译为跨域资源共享)是一种基于 HTTP 头的机制,该机制通过允许服务器标示除了它自己以外的其他 Origin源(域、协议或端口),使得浏览器允许这些源访问加载自己的资源。
跨源资源共享还通过一种机制来检查服务器是否会允许要发送的真实请求,该机制通过浏览器发起一个到服务器托管的跨源资源的 preflight(“预检”)请求。在预检中,浏览器发送的头中标示有 HTTP 方法和真实请求中会用到的头。
跨源 HTTP 请求的一个例子:运行在 https://domain-a.com
的 JavaScript 代码使用 XMLHttpRequest
来发起一个到 https://domain-b.com/data.json
的请求。
出于安全性,浏览器限制脚本内发起的跨源 HTTP 请求。例如,XMLHttpRequest
和 Fetch API 遵循同源策略。这意味着使用这些 API 的 Web 应用程序只能从加载应用程序的同一个域请求 HTTP 资源,除非响应报文包含了正确 CORS 响应头。
如下图,首先从 domain-a.com 加载了 index.html,则从 index.html(origin=domain-a.com) 加载layout.css(origin=domain-a.com) 和 image.png(origin=domain-a.com) ,则都是同源的。
但是,如果从 index.html(origin=domain-b.com) 加载 layout.css(origin=domain-a.com) 和 image.png(origin=domain-a.com) ,则是跨域的(即 origin 不同)。
CORS 机制允许 Web 应用服务器进行跨源访问控制,从而使跨源数据传输得以安全进行。现代浏览器支持在 API 容器中(例如 XMLHttpRequest
或 Fetch)使用 CORS,以降低跨源 HTTP 请求所带来的风险。
2.2 功能概述
跨源资源共享标准新增了一组 HTTP 标头字段,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET
以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST
请求),浏览器必须首先使用 OPTIONS
方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨源请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(例如 Cookie 和 HTTP 认证相关数据)。
CORS 请求失败会产生错误,但是为了安全,在 JavaScript 代码层面无法获知到底具体是哪里出了问题。你只能查看浏览器的控制台以得知具体是哪里出现了错误。
2.3 场景示例
2.3.1 简单请求
比如说,假如站点 https://foo.example
的网页应用想要访问 https://bar.other
的资源。foo.example
的网页中可能包含类似于下面的 JavaScript 代码:
const xhr = new XMLHttpRequest();
const url = "https://bar.other/resources/public-data/";
xhr.open("GET", url);
xhr.onreadystatechange = someHandler;
xhr.send();
以下是浏览器发送给服务器的请求报文:
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
。
让我们来看看服务器如何响应:
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: *
值表明,该资源可以被任意外源访问。
使用 Origin
和 Access-Control-Allow-Origin
就能完成最简单的访问控制。如果 https://bar.other
的资源持有者想限制他的资源只能通过 https://foo.example
来访问(也就是说,非 https://foo.example
域无法通过跨源访问访问到该资源),他可以这样做:
Access-Control-Allow-Origin: https://foo.example
2.3.2 Preflighted requests:预检请求
与简单请求不同,“需预检的请求”要求必须首先使用 OPTIONS
方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。"预检请求“的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。
如下是一个需要执行预检请求的 HTTP 请求:
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);
});
上面的代码使用 POST
请求发送一个 XML 请求体,该请求包含了一个非标准的 HTTP X-PINGOTHER
请求标头。这样的请求标头并不是 HTTP/1.1 的一部分,但通常对于 web 应用很有用处。另外,该请求的 Content-Type
为 application/xml
,且使用了自定义的请求标头,所以该请求需要首先发起“预检请求”。
下面是服务端和客户端完整的信息交互。首次交互是预检请求/响应:
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
从上面的报文中,我们看到,第 1 - 10 行使用 OPTIONS
方法发送了预检请求,浏览器根据上面的 JavaScript 代码片断所使用的请求参数来决定是否需要发送,这样服务器就可以回应是否可以接受用实际的请求参数来发送请求。OPTIONS 是 HTTP/1.1 协议中定义的方法,用于从服务器获取更多信息,是安全的方法。该方法不会对服务器资源产生影响。注意 OPTIONS 预检请求中同时携带了下面两个标头字段:
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
标头字段 Access-Control-Request-Method
告知服务器,实际请求将使用 POST
方法。标头字段 Access-Control-Request-Headers
告知服务器,实际请求将携带两个自定义请求标头字段:X-PINGOTHER
与 Content-Type
。服务器据此决定,该实际请求是否被允许。
第 12 - 21 行为预检请求的响应,表明服务器将接受后续的实际请求方法(POST
)和请求头(X-PINGOTHER
)。重点看第 15 - 18 行:
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
服务器的响应携带了 Access-Control-Allow-Origin: ``https://foo.example
,从而限制请求的源域。同时,携带的 Access-Control-Allow-Methods
表明服务器允许客户端使用 POST
和 GET
方法发起请求(与 Allow
响应标头类似,但该标头具有严格的访问控制)。
标头字段 Access-Control-Allow-Headers
表明服务器允许请求中携带字段 X-PINGOTHER
与 Content-Type
。与 Access-Control-Allow-Methods
一样,Access-Control-Allow-Headers
的值为逗号分割的列表。
最后,标头字段 Access-Control-Max-Age
给定了该预检请求可供缓存的时间长短,单位为秒,默认值是 5 秒。在有效时间内,浏览器无须为同一请求再次发起预检请求。以上例子中,该响应的有效时间为 86400 秒,也就是 24 小时。请注意,浏览器自身维护了一个最大有效时间,如果该标头字段的值超过了最大有效时间,将不会生效。
预检请求完成之后,发送实际请求:
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]
2.4 header
2.4.1 http request header
2.4.1.1 Origin
Origin
标头字段表明预检请求或实际跨源请求的源站。
Origin: <origin>
2.4.1.2 Access-Control-Request-Method
Access-Control-Request-Method
标头字段用于预检请求。其作用是,将实际请求所使用的 HTTP 方法告诉服务器。
Access-Control-Request-Method: <method>
2.4.1.3 Access-Control-Request-Headers
Access-Control-Request-Headers
标头字段用于预检请求。其作用是,将实际请求所携带的标头字段(通过 setRequestHeader()
等设置的)告诉服务器。这个浏览器端标头将由互补的服务器端标头 Access-Control-Allow-Headers
回答。
Access-Control-Request-Headers: <field-name>[, <field-name>]*
2.4.2 http response header
2.4.2.1 Access-Control-Allow-Origin
响应标头中可以携带一个 Access-Control-Allow-Origin
字段,其语法如下:
Access-Control-Allow-Origin: <origin> | *
Access-Control-Allow-Origin
参数指定了单一的源,告诉浏览器允许该源访问资源。或者,对于不需要携带身份凭证的请求,服务器可以指定该字段的值为通配符“*
”,表示允许来自任意源的请求。
例如,为了允许来自 https://mozilla.org
的代码访问资源,你可以指定:
Access-Control-Allow-Origin: https://mozilla.org
Vary: Origin
如果服务端指定了具体的单个源(作为允许列表的一部分,可能会根据请求的来源而动态改变)而非通配符“*
”,那么响应标头中的 Vary
字段的值必须包含 Origin
。这将告诉客户端:服务器对不同的 Origin
返回不同的内容。
2.4.2.2 Access-Control-Expose-Headers
在跨源访问时,XMLHttpRequest
对象的 getResponseHeader()
方法只能拿到一些最基本的响应头,Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma,如果要访问其他头,则需要服务器设置本响应头。
Access-Control-Expose-Headers
头将指定标头放入允许列表中,供浏览器的 JavaScript 代码(如 getResponseHeader()
)获取。
Access-Control-Expose-Headers: <header-name>[, <header-name>]*
例如:
Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header
这样浏览器就能够通过 getResponseHeader
访问 X-My-Custom-Header
和 X-Another-Custom-Header
响应头了。
2.4.2.3 Access-Control-Max-Age
Access-Control-Max-Age
头指定了 preflight 请求的结果能够被缓存多久,请参考本文在前面提到的 preflight 例子。
Access-Control-Max-Age: <delta-seconds>
delta-seconds
参数表示 preflight 预检请求的结果在多少秒内有效。
2.4.2.4 Access-Control-Allow-Credentials
Access-Control-Allow-Credentials
头指定了当浏览器的 credentials
设置为 true 时是否允许浏览器读取 response 的内容。当用在对 preflight 预检测请求的响应中时,它指定了实际的请求是否可以使用 credentials
。请注意:简单 GET
请求不会被预检;如果对此类请求的响应中不包含该字段,这个响应将被忽略掉,并且浏览器也不会将相应内容返回给网页。
2.5 解决方案
2.5.1 nginx 反向代理
正向代理:a -> c -> b
因为 a 希望访问 b,但无法直接访问。所以设置 client c 为 client a 的正向代理,即 a 的请求,都由 c 发出。
反向代理:a -> c <- b
因为 a 希望访问 c, 但 c 的内容其实来自 b,server c 是 server b 的反向代理。
比如浏览器输入 http://192.168.2.99/website 访问主页,会命中下文的 nginx 配置文件:首先 listen 了 80 端口,其次请求的路径命中了 location /,最终返回 $root/website.html(因为 tryfiles 是 $uri.html),其中 $root 即为 /opt/frontend-project/latest/,即最终访问的是 /opt/frontend-project/latest/website.html。
server {
listen 80;
root /opt/frontend-project/latest/;
index index.html index.htm;
# Make site accessible from http://localhost/
server_name _;
location / {
# First attempt to serve request as file, then
# as directory, then fall back to displaying a 404.
try_files $uri $uri.html /index.html;
# Uncomment to enable naxsi on this location
# include /etc/nginx/naxsi.rules
}
}
浏览器拿到 nginx 静态资源服务器返回的 index.html 之后,即会继续加载 html 里嵌套的 css 和 js。js 访问一个地址时,可能跨域(主要是端口号)。因为 html/css/js 的源是 80 端口(即 nginx 静态资源服务器)。但期望加载的数据是 34567 端口(即后端 http 接口 server)。因为 port 不同,所以不同源,即跨域。浏览器因为安全规范会拒绝此请求。
为了解决跨域问题,有两种方式:
- 方式一:都用后端 http 接口 server做源,即用后端 http 接口 server,来代理前端静态资源(html/css/js),这样静态资源和接口都是同一个源(即后端的)。但这目前为了开发效率,都采用了微服务的前后端项目分离架构,为了分离部署,不希望后端还耦合前端的部署。
- 方式二:都用 nginx server 做源。即 nginx 提供静态资源,还提供后端 http 接口 server 的反向代理。
方式二 nginx 配置的反向代理部分,如下:
server {
listen 80;
root /opt/frontend-project/latest/;
index index.html index.htm;
# Make site accessible from http://localhost/
server_name _;
location / {
# First attempt to serve request as file, then
# as directory, then fall back to displaying a 404.
try_files $uri $uri.html /index.html;
# Uncomment to enable naxsi on this location
# include /etc/nginx/naxsi.rules
}
location /proxy/backend-project1/ {
proxy_pass http://127.0.0.1:34567/;
}
location /proxy/backend-project2/ {
proxy_pass http://127.0.0.1:30000/;
}
location /ws {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Proxy "";
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://127.0.0.1:34567;
}
}
2.5.2 预检请求 + 后端添加 response header 解决跨域
浏览器先预检请求询问b, b 通过 response header 允许a访问。b 的 response header 示例如下:
header('Access-Control-Allow-Origin:*');//允许所有来源访问
header('Access-Control-Allow-Method:POST,GET');//允许访问的方式
具体操作方式:
- 前端:所有浏览器,均支持跨域,由浏览器自动完成,不需要程序员参与。
- 后端:后端判断请求,若请求希望跨域,则添加 response header,一般通过路由中间件完成。
优势:在后端可以自定义跨域规则,更灵活
缺点:会产生额外的预检请求