跨域
- 什么是跨域?
- 为什么要解决跨域?
- 怎么解决跨域?
什么是跨域?
同源策略
出于浏览器的同源策略限制。同源策略(SameOriginPolicy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说Web是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。同源策略会阻止一个域的javascript脚本和另外一个域的内容进行交互。
所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol),主机(host)和端口号(port)。当一个请求url的协议、域名、端口三者之间任意一个与当前页面url不同即为跨域
下表给出了相对http://store.company.com/dir/page.html
同源检测的示例:
URL | 结果 | 原因 |
---|---|---|
http://store.company.com/dir2/other.html | 成功 | 只有路径不同 |
http://store.company.com/dir/inner/another.html | 成功 | 只有路径不同 |
https://store.company.com/secure.html | 失败 | 不同协议 ( https和http ) |
http://store.company.com:81/dir/etc.html | 失败 | 不同端口 ( http:// 80是默认的) |
http://news.company.com/dir/other.html | 失败 | 不同域名 ( news和store ) |
localhost和127.0.0.1虽然都指向本机,但也属于跨域(host不同)
非同源限制
- 无法读取非同源网页的 Cookie、LocalStorage 和 IndexedDB
- 无法接触非同源网页的 DOM
- 无法向非同源地址发送 AJAX 请求如何解决跨域?
没有同源策略限制的两大危险场景
浏览器是从两个方面去做这个同源策略的,一是针对接口的请求,二是针对Dom的查询。
危险情景对应上述非同源限制中列举的前2点。
为什么要解决跨域?
所以同源策略是为了保护用户的信息安全而设立的。
但同时这又导致了一些正常的请求被拦截了,所以我们需要跨域 _
如何解决跨域?
CORS
跨域资源共享( CORS )机制允许 Web 应用服务器进行跨域访问控制,从而使跨域数据传输得以安全进行。现代浏览器支持在 API 容器中(例如 XMLHttpRequest
或 Fetch )使用 CORS,以降低跨域 HTTP 请求所带来的风险。
跨域资源共享标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。
另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET
以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST
请求),浏览器必须首先使用 OPTIONS
方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。
服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)。
CORS请求失败会产生错误,但是为了安全,在JavaScript代码层面是无法获知到底具体是哪里出了问题。你只能查看浏览器的控制台以得知具体是哪里出现了错误。
两种请求
某些请求不会触发 CORS 预检请求。我们称之为“简单请求”,若请求满足所有下述条件,则该请求可视为“简单请求”:
-
使用下列方法之一:
-
Fetch 规范定义了对 CORS 安全的首部字段集合,不得人为设置该集合之外的其他首部字段。该集合为:
Accept
Accept-Language
Content-Language
Content-Type
(需要注意额外的限制)DPR
Downlink
Save-Data
Viewport-Width
Width
-
Content-Type
的值仅限于下列三者之一:
text/plain
multipart/form-data
application/x-www-form-urlencoded
-
请求中的任意
XMLHttpRequestUpload
对象均没有注册任何事件监听器;XMLHttpRequestUpload
对象可以使用XMLHttpRequest.upload
属性访问。 -
请求中没有使用
ReadableStream
对象。
凡是不同时满足上面两个条件,就属于非简单请求。
浏览器对这两种请求的处理,是不一样的:
- 对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个
Origin
字段。 - 对于非简单请求,浏览器会自动发送预检请求。
预检请求
与前述简单请求不同,“需预检的请求”要求必须首先使用 OPTIONS
方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。"预检请求“的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。
对于nginx,如果请求方式是PUT,DELETE,COPY,MOVE,MKCOL等webdav标准的请求方式,需要nginx在编译时安装http_dav_module,相关命令为--with-http_dav_module
预检请求发送的header内容有:
Access-Control-Request-Headers: xiaotongli # 真正请求会携带的header
Access-Control-Request-Method: POST # 真正请求所用的请求方式
Origin: http://www.a.com # 真正请求的源域
如果Access-Control-Allow-Origin 的值设置为非星号,如: http://foo.example
(注意,这里的协议、域名都是需要的,端口不写,则请求头的orgin也不能写,即使是80端口也不行)。
解决案例
解决跨域的根本方式:在http的response header里添加允许跨域的信息,所以可以在web服务器端(nginx,Apache等)做、也可以在服务器语言(php,go,node等)里加,或者客户端收取http response之前(如CORS插件)
Nginx端
对应前后端分离,各自有相应域名的情况下,在前端域名下配置一个location块,让路由规则相符的请求走相应配置,将请求的内容封装好后统一发给真正的后端域名,如下:
location /api {
proxy_pass https://okr-api-ready.123u.com/;
}
也可以调用端(前端)域名nginx不作处理,服务器端配置如下:
set $cors_origin '';
if ($http_origin = 'http://www.a.com') {
set $cors_origin $http_origin;
}
if ($http_origin = 'http://www.b.com') {
set $cors_origin $http_origin;
}
add_header 'Access-Control-Allow-Origin' $cors_origin;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS, PATCH';
add_header 'Access-Control-Allow-Headers' xxxt; # 自定义的header
# $http_access_control_request_headers 可以获取全部的request headers
add_header 'Access-Control-Allow-Credentials' true;
add_header 'Access-Control-Max-Age' 86400; # 单位:秒
add_header 'Access-Control-Expose-Headers' dog;
if ($request_method = OPTIONS){
return 200;
}
-
Access-Control-Allow-Origin可以设置为*,具体的url(协议+域名+端口),$http_origin(这是一个nginx变量,动态获取origin)
-
如果想设置多个域名跨域,但 Access-Control-Allow-Origin不支持设置多个域名,就算写了也不生效。可以对$http_origin做判断,如上面配置所示
-
Access-Control-Allow-Methods不支持*;不写默认只支持GET、POST、HEAD;
-
附带身份凭证的请求与通配符,CORS请求默认不发送Cookie和HTTP认证信息。如果要把Cookie发到服务器,一方面要服务器同意,指定
Access-Control-Allow-Credentials
字段为true。另一方面,开发者必须在AJAX请求中打开withCredentials
属性。
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
// jquery中使用
$.ajaxSetup({crossDomain: true, xhrFields: {withCredentials: true}});
否则,即使服务器同意发送Cookie,浏览器也不会发送。或者,服务器要求设置Cookie,浏览器也不会处理。但是,如果省略withCredentials
设置,有的浏览器还是会一起发送Cookie。这时,可以显式关闭withCredentials
。
xhr.withCredentials = false;
需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin
就不能设为星号,必须指定明确的、与请求网页一致的域名。 同时,Cookie依然遵循同源政策,只有用服务器域名(一级域名相同即可)设置的Cookie才会上传,其他域名的Cookie并不会上传。
-
CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。
-
首部字段
Access-Control-Max-Age
表明该响应的有效时间为 86400 秒,也就是 24 小时。在有效时间内,浏览器无须为同一请求再次发起预检请求。请注意,浏览器自身维护了一个最大有效时间,如果该首部字段的值超过了最大有效时间,将不会生效。
proxy_pass的配置细节
对于请求:http://www.a.com/server/product/12?mx=12
location /server/ {
proxy_pass 'https://m.mm.top'; # 1
}
location /server {
proxy_pass 'https://m.mm.top'; # 2
}
location /server/ {
proxy_pass 'https://m.mm.top/'; # 3
}
location /server {
proxy_pass 'https://m.mm.top/'; # 4
}
以上4种配置效果说明:
http://www.a.com称为原域名,https://m.mm.top称为新域名
- 1,2情况下,proxy_pass的域名后不带斜杠/,最终效果是新域名(无斜杠)替换了原域名(无斜杠),故:
- 1的最终地址是https://m.mm.top/server/product/12?mx=12
- 2的最终地址是https://m.mm.top/server/product/12?mx=12
- 1,2的最终地址一致,即proxy_pass的域名后无斜杠,则location后的表达式是否有斜杠不影响最终地址
- 3,4情况下,proxy_pass的域名后带斜杠/,最终效果是新域名(有斜杠)替换了原域名(无斜杠)+ location的表达式(以斜杠开头),故:
- 3的最终地址是https://m.mm.top/product/12?mx=12
- 4的最终地址是https://m.mm.top//product/12?mx=12
- 注意4的最终地址多了一个斜杠,故proxy_pass的域名后有斜杠,location的表达式最后是否有斜杠效果是不同的
PHP端修改header
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
header('HTTP/1.1 200');
}
if (isset($_SERVER["HTTP_ORIGIN"])) {
header('Access-Control-Allow-Origin:'.$_SERVER["HTTP_ORIGIN"]);
}
header('Access-Control-Allow-Methods:GET,POST,PUT,DELETE,OPTIONS,PATCH');
header('Access-Control-Allow-Headers:Xiaotongli');
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Max-Age: 3');
header('Access-Control-Expose-Headers:pig,dog');
- Access-Control-Allow-Origin这个header头,如果nginx和php同时设置的话,预检请求会报设置了多个origin,导致失败。
- 经测试,对于预检请求,在php里设置OPTIONS返回的状态码,2开头的都可以通过,nginx里设置的返回码,只有200到204可以预检通过。
注:
- 前端手动设置origin来模拟会被浏览器拒绝
参考:
- https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS
- http://www.ruanyifeng.com/blog/2016/04/cors.html