这是一次组内的学习分享,按照自己学习的过程作为时间线来讲述
Part1 跨域
浏览器的同源策略
故事的开始,这是我第一次接触到跨域:
1 |
|
先来看看为什么浏览器会报错:
浏览器的同源策略:作为安全策略,它用于限制一个 origin 的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。——《MDN 浏览器的同源策略》
所以这个报错是因为我们触发了浏览器的同源策略导致的。
那什么是同源呢?从下图可知:
所以当两个 URL 协议、域名、端口全都相同的时候,这两个 URL 是同源。
但是很多情况下,我们页面会引用到外部的css
和js
文件,以及通过像img
video
audio
这类 HTML 标签引入外部资源,这都属于跨域。
为了避免页面请求不到外部文件导致样式错乱、或导致图片无法加载的问题,同源策略规定了通过 HTML 标签引入外部文件的时候予以放行。例如:
<img>
:引入外部图片<link>
:引入外部 css<script>
:引入外部 javascript- …..
以上的情况属于跨域资源嵌入,这一般是允许的,但是跨域读操作却是被禁止的!!
而像发送Ajax
请求、获取DOM
、js
对象都属于跨域读操作。
我的Ajax
请求也正因为这个原因被同源策略禁止了。
转念一想,自从前后端分离后,前端网页和业务数据接口服务器常常不在一起,分属不同的域名或者使用不同的端口,前端请求接口显然会发生跨域,那业内普遍都是如何解决的呢?
跨域的多种解决方案
- JSONP
”既然规则中允许加载外部 JS 文件,我们为什么不利用它来实现外部接口的请求呢?“
- Nginx 代理
“因为跨域现象只在浏览器中存在,所以只要代理服务器将跨域请求转发即可”
比如前端运行在localhost:8080
, 接口放在localhost:9527
在 http://localhost:8080/
发送请求到http://localhost:8080/api/getData
通过代理后它其实获取的是 http://localhost:9527/getData
接口的数据。
下面是 conf 文件夹下的 nginx.conf
配置文件的内容:
1 |
|
- Webpack
通过 webpack-dev-server 代理服务转发跨域请求,和 Nginx 一样都是通过代理实现
package.json 文件,在 devDependencies 前加入如下代码:
1 |
|
webpack.config.js :
1 |
|
在命令行中执行命令yarn serve
启动开发服务,会看浏览器打开地址为 http://0.0.0.0:8080/
的页面,发送请求到http://localhost:8080/api/getData
,通过代理后它其实获取的是 http://localhost:9527/getData
接口的数据。
需要注意的是:用 webpack 开发环境处理了跨域,打包后要部署到生产环境的代码只是静态文件,是没有解决跨域的,不要误以为在开发环境用 webpack 处理了跨域,生产环境也就处理好了。
- postMessage+iframe(拍机堂与中台的收银台交互场景)
window.postMessage(message, targetOrigin)
方法是 html5 新引进的特性,可以使用它来向其它的 window 对象发送消息,无论这个 window 对象是属于同源或不同源
下面是父页面通过 iframe 嵌入子页面后,用postMessage
通信的 code demo:
父页面(localhost:80
):
1 |
|
1 |
|
子页面(localhost:81
):
1 |
|
1 |
|
- CORS(W3C标准 Cross-Origin Resource Sharing,跨域资源共享)
CORS原理
CORS(跨源资源共享)是 W3C 的一个工作草案,定义了在访问跨源资源时,浏览器与服务器应该如何沟通。 Firefox 3.5+、Safari 4+、Chrome、iOS 版 Safari 都通过 XMLHttpRequest对象实现了对 CORS 的原生支持。在尝试打开不同来源的资源时,无需额外编写代码就可以触发这个行为。
——摘自《Javascript高级程序设计》
CORS应该算是现在最为推荐的跨域处理方案。不像JSONP那样投机取巧,走的是正规路子,不仅适用于各种Method,而且更加方便和简单。当然只有现代浏览器支持。
他们在正式的跨域请求之前,先发送了一个OPTIONS
请求去询问服务器是否允许接下来的跨域请求
“这怎么个询问法呢?”
浏览器和网站服务器商定了一下,在OPTIONS请求里新增了几个字段:
Origin
:发起请求原来的域Access-Control-Request-Method
:将要发起的跨域请求方式Access-Control-Request-Headers
:将要发起的跨域请求中包含的请求头字段
服务器在响应字段中来表明是否允许这个跨域请求,浏览器收到后检查如果不符合要求,就拒绝后面的请求
Access-Control-Allow-Origin
:允许哪些域来访问(*表示允许所有域的请求,但不能携带cookie)Access-Control-Allow-Methods
:允许哪些请求方式Access-Control-Allow-Headers
:允许哪些请求头字段Access-Control-Allow-Credentials
:是否允许携带CookieAccess-Control-Max-Age
询问结果的有效期(下面细说)
可以从下图来理解预请求的过程:
“每次请求都要发送预请求是不是太麻烦了???”
这里为了避免每次都要询问,CORS做了两个重要的优化:
1.第一,如果是一个简单请求,那就直接发起请求,只需在请求中加入Origin
字段表明自己来源,在响应中检查**Access-Control-Allow-Origin**
,如果不符合要求就报错。
2.服务器响应字段中有一个Access-Control-Max-Age
,它表明了这个询问结果的有效期,只要在有效期内,也不必再次询问。
那么下面就用CORS解决我遇到的跨域问题:
因为只需要后台做处理就可以,前端直接访问接口全路径。
Node实现如下:
demo1:
1 |
|
demo2:
$ npm install cors
1 |
|
到这里,我遇到的跨域问题已经解决了,这里总结一下都学到了什么:
- 浏览器的同源策略
- 同源策略对跨域操作有什么限制
- 跨域的多种解决方案
- CORS原理
- node如何实现cors
Q&A
有了跨域知识基础,现在我们看另外两个问题:
a.跨域请求是否会主动携带cookie
默认情况下,跨源请求不提供凭据(cookie、HTTP 认证及客户端 SSL 证明等)。
通过将withCredentials 属性设置为 true,可以指定某个请求应该发送凭据。如果服务器允许带凭据的请求,会用下面的 HTTP 头部来响应:Access-Control-Allow-Credentials: true
——摘自《Javascript高级程序设计》
b. 跨域请求接口响应头设置 cookie 能否成功
在未发生跨域的情况下,cookie设置的流程:
- 客户端发送 HTTP 请求到服务器
- 当服务器收到 HTTP 请求时,在响应头里面添加一个 Set-Cookie 字段 ✅
- 浏览器收到响应后种下 Cookie
- 之后对该服务器每一次请求中都通过 Cookie 字段将 Cookie 信息发送给服务器。
最后经过在网上的搜寻再结合实测,得出结论:
- 前端设置withCredentials:true,后端即使不设置CORS头也可set-cookie成功
- 前端不设置withCredentials或者false,set-cookie响应头会被直接忽略
PART2 跨站
新的问题又出现了😵💫
Chrome版本升级后,很多中后台登录成功后Cookie无法传递导致登录状态失效
这段话翻译大致如下:Google发布的 Chrome 80 版本,将没有声明SameSite值的cookie默认设置为SameSite=Lax,以此来减少非安全第三方 cookie 的使用。
那么问题来了
1.第三方是什么?
2.SameSite属性是什么?
3.Chrome为什么要这样做,我们要如何应对?
第三方(跨站)是什么???😵💫
跨站是与同站相反的概念,同站是指两个 URL 的 eTLD+1 相同, 其中,eTLD 表示有效顶级域名,注册于 Mozilla 维护的公共后缀列表(Public Suffix List)中,例如,.com、.co.uk、.github.io 等。eTLD+1 则表示,有效顶级域名+二级域名.
PS: 不需要考虑端口,但要考虑协议:在 Chrome 86/Firefox 79 中,浏览器增加了一个 Schemeful Same Site 的选项,将协议也增加到了 Same Site 的判断规则中。但是并不是完全的不等判断,可以理解是否有 SSL 的区别。例如 http://
和 https://
跨站,但 wss://
和 https://
则是同站,ws://
和 http:/
也算是同站。
举几个例子🌰:
www.baidu.com & www.taobao.com 跨站
www.m.taobao.com & www.pc.taobao.com 同站
jack.github.io & rose.github.io 跨站还是同站呢?
所以,如果我们本地请求 sjapi.aihuishou.com/recycler-api/quotation/document/query
本地URL需要是 xxx.aihuishou.com 这种格式,才不会跨站。
SameSite是什么?
SameSite 是HTTP响应头 Set-Cookie 的属性之一。
它允许您声明该Cookie是否仅限于同站或者同站上下文。
属性值 | 说明 |
---|---|
None | Cookie将在所有上下文中发送,即允许跨域发送 |
Lax(default) | Cookies允许与顶级导航一起发送,并将与第三方网站发起的GET请求一起发送。这是浏览器中的默认值 |
Strict | Cookies只会在第一方上下文中发送,不会与第三方网站发起的请求一起发送 |
以前 None
是默认值,但最近浏览器版本将 Lax
作为默认值,用来防御CSRF攻击。
我们中后台系统会不会被影响呢?
看看从 None
改成 Lax
到底影响了哪些地方的 Cookies 的发送:
请求类型 | 实例 | 以前 | Strict | Lax | None |
---|---|---|---|---|---|
None | <a href="..."></a> |
发送cookie | 不发送 | 发送cookie | 发送cookie |
预加载 | <link rel="prerender" href="..."/> |
发送cookie | 不发送 | 发送cookie | 发送cookie |
GET表单 | <form method="get" action="..."/> |
发送cookie | 不发送 | 发送cookie | 发送cookie |
POST表单 | <form method="post" action="..."/> |
发送cookie | 不发送 | 不发送 | 发送cookie |
Iframe | <iframe src="..."/> |
发送cookie | 不发送 | 不发送 | 发送cookie |
Ajax | $.get("...") |
发送cookie | 不发送 | 不发送 | 发送cookie |
Image | <img src="..."/> |
发送cookie | 不发送 | 不发送 | 发送cookie |
iframe 嵌入的 web 应用有很多是跨站的,都会受到影响。
由于我们 FAT/UAT/生产环境 的URL的 eTLD+1
都是 aihuishou.com
, 和请求接口的URL的eTLD+1
一致, 所以除了我们开发环境和一些跨站的iframe
会受到影响,其余的没什么影响。
跨站请求的解决方案
知道原理后,答案就显而易见了,设置 SameSite 为 none 即可!
以 Adobe 网站为例,查看请求可以看到:
这里有两点要注意的地方:
- HTTP 接口不支持 SameSite=none
- 需要 UA 检测,部分浏览器不能加 SameSite=none
因为我们用的大多数都是HTTP接口,所以不适合用该方案
既然不支持三方cookie,那么和跨域同理,也可以使用反向代理解决(nginx
)
目前的解决方案是用SwitchHosts切换我们的hosts:
1 |
|
如果使用的是Firefox,可以做如下配置,也能解决samesite导致的开发环境的问题:
Firefox打开 about:config
1 |
|
Q&A
学完了跨站,最后两个问题:
c.跨站请求能否携带 cookie
由于chrome80版本后,cookie的samesite属性的默认值从None变为了Lax,所以跨站请求默认不携带cookie。
d. 跨站请求接口响应头设置 cookie 能否成功
- 前端设置withCredentials:true,后端即使不设置CORS头也可set-cookie成功
- 前端不设置withCredentials或者false,set-cookie响应头会被直接忽略