持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第17天,点击查看活动详情
什么是同源策略
同源策略是一个重要的安全策略,它用于限制一个源的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。
所谓同源,就是指协议、域名、端口都要相同,只要有其中一个不同就会产生跨域。
为什么会有同源策略
- 为了防止恶意网页可以获取其他网站的本地数据。
- 为了防止恶意网站在自己网站有访问其他网站的权利,以免通过 cookie 免登,拿到数据。
什么是跨域
跨域指浏览器不能执行其他网站的脚本,它是由浏览器的同源策略造成的,是浏览器对 JavaScript 施加的安全限制。
注意:跨域并不是请求发不出去,服务端也能收到请求并正常返回结果,只是结果被浏览器拦截了。
解决跨域的九种方法
JSONP
原理:利用 <script>
标签没有跨域限制的漏洞,网页可以得到从其他来源动态产生的 JSON 数据, JSONP 请求一定需要对方的服务器做支持才可以
优缺点:JSONP 简单且兼容性好,可用于解决主流浏览器的跨域数据访问的问题;但仅支持 get 方法,因为 script 脚本的请求方式是 GET。
实现流程:
- 创建一个
<script>
并载入页面中,src 是跨域的 api 接口地址,但后面需要带上一个标记有回调函数的请求参数,如http://10.92.191.223:3000/test/?callback=handleCallback
。 - 后端接受到请求后需要进行特殊的处理,将回调函数名和数据拼接成一个函数调用的形式返回给前端,如
handleCallback({"status": "success", "message": "跨域成功"})
。
因为是 script 脚本,所以前端请求到这个脚本后会立即执行这个脚本内容,即调用这个回调函数。
代码实现:
function jsonp(url, callback) {
let script = document.createElement('script');
let fn = Symbol();
window[fn] = function(response) {
try{
callback(response);
} finally {
delete window[fn];
document.body.removeChild(script);
}
}
script.type = 'text/javascript';
if (url.indexOf('?') === -1) {
url += `?callback=${fn}`;
} else {
url += `callback=${fn}`;
}
script.src = url;
document.body.appendChild(script);
}
CORS
概念:CORS 即跨域资源共享,当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,会发起一个跨域 HTTP 请求,它使用额外的 HTTP 头来告诉浏览器,让运行在一个 origin 上的 web 应用被准许访问来自不同源服务器上的指定的资源。
类别:CORS 分为 简单请求 和 非简单请求。
满足以下两个条件,就可以看做是 简单请求:
- 请求方法是以下三种方法之一:
- GET
- POST
- HEAD
- HTTP 的头信息不超出以下几种字段:
- Accept
- Accept-Language
- Content-Language
- Content-Type:只限于三个值
application/x-www-form-urlencoded
、multipart/form-data
、text/plain
若不满足以上条件,就属于非简单请求了。
简单请求
基本流程:浏览器直接发出 CORS 请求,会在头信息中增加一个 Origin
字段,表明本次请求来自哪个源(协议+域名+端口),浏览器根据这个字段的值来决定是否同意这次请求。
结果:
- 如果 Origin 指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段:
Access-Control-Allow-Origin
:必须。它的值要么是请求时 Origin 字段的值,要么是一个*
,表示接受任意域名的请求。Access-Control-Allow-Credentials
:可选。它的值是一个布尔值,表示是否允许发送 Cookie 。默认值为false
;若设为true
,即表示服务器明确许可 Cookie 可以包含在请求中。注意:
- 如果服务器将该字段设为
true
的话,开发者还需要在 AJAX 请求中打开withCredentials
属性:(否则即使服务器同意发送Cookie,浏览器也不会发送。或者,服务器要求设置Cookie,浏览器也不会处理。)
var xhr = new XMLHttpRequest(); xhr.withCredentials = true;
- 如果要发送 cookie ,
Access-Control-Allow-Origin
就不能设为*
,必须指定明确的、与请求网页一致的域名。同时,cookie 依然遵循同源政策,且(跨源)原网页代码中的document.cookie
也无法读取服务器域名下的的 cookie 。 - 如果省略
withCredentials
设置,有的浏览器还是会一起发送 cookie ,这时可以显式关闭withCredentials
:xhr.withCredentials = false
。
- 如果服务器将该字段设为
Access-Control-Expose-Headers
:可选。CORS 请求时,XMLHttpRequest
对象的getResponseHeader()
方法只能拿到 6 个基本字段:Cache-Control
、Content-Language
、Content-Type
、Expires
、Last-Modified
、Pragma
。如果想拿到其他字段,就必须在Access-Control-Expose-Headers
里面指定。Content-Type
:响应给浏览器的资源的类型。
- 如果 Origin 指定的域名不在许可范围内,服务器会返回一个正常的 HTTP 回应,浏览器发现回应里没有
Access-Control-Allow-Origin
字段,就知道出错了,从而抛出一个错误。
非简单请求
概念:非简单请求是指那种对服务器有特殊要求的请求,比如请求方式是 PUT
或 DELETE
等,或者 Content-Type
字段的类型是 application/json
等。
基本流程:非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,也叫预检请求。过程是这样的:浏览器先询问服务器,当前网页的域名是否在服务器的许可名单之中,以及可以使用哪些 HTTP 请求方式和头信息字段,只有得到肯定答复,浏览器才会发出正式的 HTTP 请求,否则就报错。
预检请求:
- 请求方法是
OPTIONS
,表示这个请求是用来询问的。 - 请求字段:
Origin
:表示请求来自哪个源。Access-Control-Request-Mothod
:必须。列出浏览器的 CORS 请求所有会用到的 HTTP 方法。Access-Control-Request-Headers
:该字段是一个逗号分隔的字符串,指定浏览器 CORS 请求会额外发送的头信息字段。
- 判断是否同意请求:服务器收到浏览器的预检请求,检查了头信息的三个字段后,确认允许跨源请求,就可以做出回应。如果返回的头信息中有
Access-Control-Allow-Origin
这个字段就代表允许跨域请求;如果没有,就代表不同意这个预检请求,就会报错。 - 同意请求要回应的字段:
Access-Control-Allow-Origin
: 允许跨域的源地址。只要服务器通过了预检请求,在以后每次的 CORS 请求都会自带一个Origin
头信息字段,服务器的回应,也都会有一个Access-Control-Allow-Origin
头信息字段。Access-Control-Allow-Methods
: 必需。它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。这是为了避免多次"预检"请求。Access-Control-Allow-Headers
: 如果浏览器请求包括Access-Control-Request-Headers
字段,则该字段是必需的。服务器支持的所有头信息字段。Access-Control-Allow-Credentials
: 表示是否允许发送 Cookie 。Access-Control-Max-Age
: 可选。用来指定本次预检请求的有效期,单位为秒。
- 否定请求回应:
- 返回一个正常的 HTTP 回应,但是没有任何 CORS 相关的头信息字段。这时,浏览器就会认定服务器不同意预检请求。
正式请求:其实就是简单请求的流程。
所以,非简单请求 = 预检请求 + 简单请求 。
nginx 反向代理
原理:本质和 CORS 跨域原理一样,通过配置文件设置请求响应头如 Access-Control-Allow-Origin…
等字段实现跨域。
实现:通过 Nginx 配置一个代理服务器(域名与跨域页面的域名相同,端口不同)做跳板机,反向代理访问被跨域页面的域名接口,并且可以顺便修改 cookie 中 domain 信息,方便当前域c ookie 写入,实现跨域访问。
nginx具体配置:
server {
listen 81;
server_name www.domain1.com;
location / {
proxy_pass http://www.domain2.com:8080; #反向代理
proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
index index.html index.htm;
// 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
add_header Access-Control-Allow-Origin http://www.domain1.com; // 当前端只跨域不带cookie时,可为*
add_header Access-Control-Allow-Credentials true;
}
}
document.domain + iframe
原理:两个页面都通过 js 强制设置 document.domain
为基础主域,就实现了跨域。
实现:
- 父窗口:
https://domain.com/a.html
<iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
<script>
document.domain = 'domain.com';
var user = 'admin';
</script>
- 子窗口:
https://child.domain.com/b.html
<script>
document.domain = 'domain.com';
// 获取父窗口中变量
console.log('get js data from parent ---> ' + window.parent.user);
</script>
location.hash + iframe跨域
实现原理:a 想要与 b 跨域相互通信,需通过中间页 c 来实现。 三个页面,不同域之间利用 iframe 的 location.hash
传值,相同域之间直接 js 访问来通信。
其他
除此之外,还有其他的方法可以解决跨域问题,如:
- node 中间件
- websocket