- canvas中使用跨域图片如何做认证
- 浏览器什么时候认为请求是cross-site,此时如何携带cookie
- 同名cookie如何确定优先级
- SameParty提案为了解决什么问题
跨域、cookie是校招面试高频基础问题。本文用一个模拟案例,希望能让我们在MDN之外,有个实战的理解,为面试出题提供新的素材。
相关内容:跨域
,跨站
, cookie
,SameSite
,SameParty
项目起步
开发一个网站,部署在shop.taobao.com上,所有img等资源和后端接口都通过https://shop.taoba...
同源策略:SOP是同源策略(Same origin policy)的缩写,是一种由美国网景通信公司提出的安全策略。其原理是在Web浏览器中,不同源的文档脚本之间不能相互访问。这种策略可以有效防止跨站脚本攻击。
前后端分离
随着项目推进,前端服务器和后端接口压力不同,不想再用前端服务器反向代理后端接口,开始前后端分离,后端独立部署于api.taobao.com。此时域名不同,导致跨域。此时可以在devtools网络面板中看到请求标头的
Sec-Fetch-Mode: cors,控制台也会有相应报错。
跨域限制
- DOM:禁止操作非源页面的DOM与JS对象
这里主要场景是iframe跨域的情况,非同源的iframe是限制互相访问的
- XmlHttpRequest:禁止使用XHR对象向不同源的服务器发起HTTP请求,即不能发送跨域ajax请求
- 本地存储:Cookie、LocalStorage和IndexDB无法跨域读取
解决方式除了代理到同源,常用的就是后端配合或jsonp,本文不讨论jsonp。
后端设置允许跨域
设置允许的源
makefile
Access-Control-Allow-Origin: <origin> | *
表示允许跨域访问的单一源,或则为所有源,但是不安全,不建议使用。
如果是简单的GET请求,api.taobao.com想被https://shop.taoba...
那如果另一个域名 mall.taobao.com也想访问后端接口https://api.taobao...
设置完origin之后有些get请求已经可以正常访问了,可是有些post却不行。
设置允许的方法和标头
预检
查看网络面板可以看到这些失败的请求之前都发送了OPTIONS预检请求。
与简单请求不同,"需预检的请求"要求必须首先使用
OPTIONS
方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。"预检请求"的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。
简单请求需要满足以下几个条件,不满足条件的都是"需预检的请求"
- 请求方法
- get
- head
- post
- http的头信息不超过以下几种字段
- Accept
- Accept-Language
- Content-Type:(只限于application/x-www-form-urlencoded、multipart/form-data、text/plain)
- Content-Language
- Last-Event-ID
-
请求中的任意
XMLHttpRequest
对象均没有注册任何事件监听器;XMLHttpRequest
对象可以使用XMLHttpRequest.upload
属性访问。 -
请求中没有使用
ReadableStream
对象
现在大多数接口都是content-type: application/json,那自然就会触发OPTIONS预检。
我们经常使用的常见的复杂请求有:
-
请求方法为put或delete
-
Content-Type字段类型为:application/json
-
添加额外的http header 比如:access_token
OPTIONS预检请求会发起一次空body的OPTIONS预检请求,会带着这两个header:
xml
Access-control-request-headers: <header-name>[, <header-name>]*
触发非简单请求的头,比如自定义头、或content-type为 application/json。如果没有就不发送。
sql
access-control-request-methods: <method>[, <method>]*
将要跨域请求的方法,比如POST
预检响应
此时服务器要处理options请求,返回相应的头:
makefile
Access-Control-Allow-Origin: <origin> | *
同上
sql
Access-Control-Allow-Methods: <method>[, <method>]*
允许跨域访问的方法,GET、POST等
xml
Access-Control-Allow-Headers: <field-name>[, <field-name>]*
同上,允许跨域访问的头,如content-type
makefile
Access-Control-Max-Age: <delta-seconds>
浏览器的预检请求可以通过设置Access-Control-Max-Age进行缓存
很多后端为了省事,并没有根据方法单独处理,针对跨域都统一加了
makefile
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: *
Access-Control-Allow-Methods: *
假设我们的后端接口api.taobao.com也是这样设置的。
cookie跨域认证
普通浏览接口现在都正常了,但是对于一些需要认证的接口无法通过认证了,cookie(cookie的具体配置后文再展开)跨域无法发送了。
Credentials
后端
yaml
Access-Control-Allow-Credentials: true
允许携带认证cookie
此时以下参数也不能再设置为*了,需要明确指定
makefile
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: *
Access-Control-Allow-Methods: *
Access-Control-Allow-Origin
根据请求来源动态填写,注意白名单控制
其他两项根据实际情况填写
注意:后端接口往往经过 网关 和代理,如果Access-Control-Allow-Origin Access-Control-Allow-Credentials被添加多次,也会导致跨域问题
csharp
has been blocked by CORS policy: The value of the 'Access-Control-Allow-Credentials' header in the response is 'true, true' which must be 'true' when the request's credentials mode is 'include'.
前端
前端需要显示的声明需要携带cookie
XMLHttpRequest
ini
const xhr = new XMLHttpRequest()
xhr.withCredentials = true
Axios
csharp
axios.get(path, {
withCredentials: false
})
fetch
perl
fetch("https://example.com", {
credentials: "include", // "same-origin" 只能同源携带
});
静态资源
随着业务发展,对资产的安全性有了一定要求。对于图片等资产也需要认证,如果认证token像ajax接口一样放在header里就行不通了,放在query里给客户的感觉也像没认证一样,毕竟复制链接就可以发送查看。所以像上述接口一样,统一采用cookie认证。
img标签
如果是同源图片,放在shop.taobao.com,cookie可以自动携带。为了优化网络加载,图片放到了 cdn.taobao.com (注意都是.taobao.com)上,此时跨域了怎么办,我们会发现,html上的img标签中的资源已经携带了cookie,查看网络面板
Sec-Fetch-Mode是no-cors,浏览器对于img,js等自身请求的资源是不当做跨域处理的,这也是jsonp的原理。(这里提个问题,如果域名是cdn.aliyun.com 需要注意什么呢)
canvas、webgl
对于普通图片,可以不做任何处理,但是业务中使用了canvas绘制图片,webgl中使用了图片作为贴图,canvas 使用跨域图片是被污染状态,不能使用getImageData toDataURL等功能,在webgl中直接使用跨域图片也是不行的。比如:
canvas.toDataURL()导出图片会报错:Uncaught DOMException: Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported.
webgl中使用跨域图片报错:Uncaught DOMException: Failed to execute 'texImage2D' on 'WebGLRenderingContext': The image element contains cross-origin data, and may not be loaded.
对于这些图片需要显示指定img标签或Image对象使用跨域模式:
ini
<img crossorigin="" src="" alt="">
<img crossorigin="anonymous" src="" alt="">
<img crossorigin="use-credentials" src="" alt="">
ini
const img = new Image()
img.crossorigin = ''
img.crossorigin = 'anonymous'
img.crossorigin = 'use-credentials'
- anonymous 对此元素的 CORS 请求不携带cookie。
- "" 设置一个空的值,如 crossorigin 或 crossorigin="",和设置 anonymous 的效果一样。
- use-credentials 对此元素的 CORS 请求将携带cookie。
我们的使用场景下,需要使用携带cookie,所以此处设置为 use-credentials,网络面板可以看到
Sec-Fetch-Mode是cors,cookie也正常携带了,canvas和webgl也能够正常使用图片。
注意缓存导致跨域
同一资源的跨域和非跨域版本
因为通常img不会刻意设置crossorigin,那么如果该资源也被canvas或webgl使用,被设置为use-credentials进行加载,那么就要小心缓存带来的问题,因为本地存在了非跨域版本的缓存,请求跨域版本的时候直接读取缓存,就会加载该版本导致跨域报错。解决方式:
- 统一加crossorigin:注意历史缓存问题
- 禁用缓存:影响性能
- 跨域版本图片加query参数如 cdn.taobao.com/img.jpg?cor... ,以和非跨域版本分别缓存:改动相对较少,影响性能
网站升级
之前的一批图片资源没有做跨域,客户端缓存。网站升级后采用跨域方式加载,也会遇到上面的问题,此时也需要留意处理,方式同上。
切换域名导致缓存跨域
当前页面是shop.taobao.com,加载了跨域图片https://cdn.taobao...
Vary
Vary 决定了对于未来的一个请求头,应该用一个缓存的回复 (response) 还是向源服务器请求一个新的回复。它被服务器用来表明在 content negotiation algorithm(内容协商算法)中选择一个资源代表的时候应该使用哪些头部信息(headers).
通俗的理解就是哪些头在被缓存的时候需要考虑,被当做缓存key,以判断是否视为同样的文件。
我们的案例为例,想要不同origin的请求得到不同的结果,设置 Vary: Origin,那么不同域下的请求就会当做不同的缓存,不会再击中同一份缓存,也就解决了跨域问题。
题外话:Vary不止用在浏览器,服务端的代理缓存同样有用,比较常用的是Vary: User-Agent,如果pc版和移动版同样路径下后端输出不同的html,或是不同浏览器下的文件有区别,nginx代理的时候就会只缓存一份,就会出现问题。那么该设置就可以缓存不同的html,防止缓存错乱。
cookie跨站认证
业务进一步发展,cdn 从cdn.taobao.com迁移到了https://cdn.aliyun...
观察面板可以看到:
Sec-Fetch-Site: cross-site 跨站了,什么是跨站?
跨站
Cookie 遵守同站策略
,而非同源策略
,Cookie
并不区分端口
与协议
。同站同源两者的区别主要在于判断的标准是不一样的。一个 URL 主要有以下几个部分组成:
可以看到同域的判断比较严格,需要 protocol
、 hostname
、port
三部分完全一致。
跨站判断主要是根据 Mozilla 维护的公共后缀表
(Pulic Suffix List)使用有效顶级域名(eTLD)+1的规则查找得到的一级域名是否相同来判断是否是同站请求。eTLD 的出现主要是为了解决 .``com.cn
、 .``com.hk
、 .co.jp
这种看起来像是二级域名的但其实需要作为顶级域名存在的场景。
有效顶级域名eTLD
域名可以分成顶级域名(TLD Top-Level Domain)、二级域名、三级域名等等,如:
顶级域名:.com
, .cn
, .top
, .xyz
, .io
二级:baidu.com
, bilibili.com
三级域名:xx.baidu.com
xx.bilibili.com
.com
是在 PSL 中记录的有效顶级域名eTLD,shop``.taobao.com
和 cdn.taobao.com
就是同站域名
.github.io
是有效顶级域名eTLD,所以 a.github.io
和 b.github.io
就是跨站域名
Cookie属性
那么如何向跨站域名发送cookie呢,其实这就是常说的第三方cookie,现在safari已经默认禁止,chrome的无痕模式默认也是禁止的,我们先忽略。
这里就要展开cookie的具体属性了。
Secure
Secure
属性是防止信息在传递的过程中被监听捕获造成信息泄漏。当 Secure
标志的值被设置为 true 时,表示创建的 Cookie
会被以安全的形式向服务器传输,即只能在 HTTPS
连接中被浏览器传递到服务器端进行会话验证,如果是 HTTP
连接则不会传递该信息,所以 Cookie
的具体内容不会被盗取,该属性只能在 HTTPS
站点(localhost不受影响)下被设置。
SameSite
首先我们最关心的影响跨站的属性,SameSite,三个可选值:
None
Lax
默认值Strict
None
在 Chrome80 版本以前,Same-Site 的默认值是None
, 该属性值表示不做任何限制,允许第三方Cookie
。
何时写入第三方cookie
cookie的domain的设置是有限制的,只能设置自身域内(被请求方的域下)。打开Devtools面板,什么时候我的网站会列出出现第三方Cookie
?
- 通过
img标签
引入一个外域的图片,或是iframe等等,服务端响应通过set-cookie
设置了Cookie
,并且设置SameSite为None,此设置同时要求Secure为true。 - iframe的网页内通过js写入SameSite为None,Secure为true的cookie,此时面板中会单独列出来:
- 请求一个
跨域请求
,服务端响应通过set-cookie
设置了Cookie
,并且设置SameSite为None,此设置同时要求Secure为true。并且,满足跨域携带cookie的前后端配置,(上文中的 《cookie跨域认证》章节),特别注意前端ajax配置对应的credentials,此配置在跨域写入cookie时也是必须的。
Lax
Lax
会对一部分第三方Cookie
进行限制发送,在 Chrome 80 中浏览器将默认的 SameSite 规则从 SameSite=None
修改为 SameSite=Lax
。设置成 SameSite=Lax
之后页面内所有跨站情况下的资源请求都不会携带 Cookie,但是导航到目标网址的 Get 请求除外------也就是:链接(点击链接相当于在当前页发起get跨域请求后跳转),预加载请求,GET 表单。具体如下:
Strict
Strict
最为严格,它完全拒绝第三方站点,实际运用场景并不多,当某些 Cookie 被设为Strict
后,可能会影响到用户的体验。比如我在baidu.com
中用a标签
链接到bilibili
,而bilibili
的token
如果是Strict
的话,那我跳转过去就会丢失登录状态。
Domain
Domain
指定了哪些主机可以接受 Cookie。如果不指定,该属性默认为同一 host 设置 cookie,不包含子域名 。如果指定了Domain
,则一般包含子域名。
set-cookie时如果指定了domain,则按照指定domain 设置,如果不以.
开头,则浏览器会自动在前面加.
,此时所有子域名都可以使用此cookie,如:
ini
document.cookie="key=value;domain=taobao.com"
此时 shop.taobao.com cdn.taobao.com 都可以使用此cookie。
set-cookie时如果没有指定domain,则按照完整域名写入,不包含子域名。如:
ini
document.cookie="key=value;"
此时只有www.taobao.com可以使用此cookie。
Path
Path
属性指定了一个 URL 路径,该 URL 路径必须存在于请求的 URL 中,以便发送 Cookie
标头。以字符 ("/") 作为路径分隔符,并且子路径也会被匹配。
Path
决定具体请求哪个路径时会被携带。
如何匹配
例如,设置 Path=/docs
,则以下地址都会匹配:
/docs
/docs/
/docs/Web/
/docs/Web/HTTP
但是这些请求路径不会匹配以下地址:
/
/docsets
/fr/docs
当未设置Path
或者设置为空时,Path
会被设置为当前请求路径
注意点:
- 当请求地址不带末尾的
/
时,www.a.com:3000/a/b
- 当请求地址末尾带
/
时,www.a.com:3000/a/b/
同名cookie优先级
Cookie是由Domain
与 Path
来区分的,因此不同的Domain
或Path
会被识别成不同的Cookie, 所以你可能会遇到多个同名的情况,那么优先级如何确定呢
path不同
如果path不同,则匹配越精确,排序越靠前,后端拿到的是path匹配最精确的值,不考虑domain。
path相同
比如在shop.taobao.com 同path下写入两个cookie:
此时请求shop.taobao.com上资源,key取哪个cookie呢,因为.shop.taobao.com更精确所以取value2么?查看网络面板发现:
value1排在前面,后端拿到的值也是value1,那么这个排序是如何决定的呢,这个顺序是 cookie 更新顺序决定的 ,先更新或写入的cookie排在前面,也就是说后端拿到的是最早更新或写入的cookie,不考虑domain。
Expires 和 Max-Age
Expires
与 Max-Age
属性定义了 Cookie 的生命周期,也就是浏览器应删除 Cookie 的时间。在默认情况下Cookie 的生命周期是 Session
级别,即退出浏览器后自动过期。 与 Http Cache
类似, Expires
是以一个绝对GMT格式的时间
的来指定过期时间,而 Max-Age
是以多少秒后过期。Max-Age
是 http1.1
的产物,优先级比 Expires
要高
删除cookie
过期时间经常用来删除cookie,只有将 Cookie 设为过期才会删除, 注意只有符合指定 domain 与 path 会被删除。
HttpOnly
HttpOnly
要求浏览器不要通过 HTTP(和HTTPS)以外的渠道使用 Cookie,也就是说只能通过 Http 的响应头里进行Set-Cookie
, 用户无法在 js 代码中去操作与读取该 Cookie。这个属性主要是用来缓解 XSS 攻击的。
跨站认证
回到最初的问题,shop.taobao.com 访问 cdn.aliyun.com资源,并认证,如何解决?
只需要在最开始调用cdn 登录接口,通过body或header传递认证token,并通过set-cookie写入属性为SameSite=None,Secure=true的token,后续资源就可以携带认证cookie了。(或者采用iframe等其他写入第三方cookie的方式)
限制
虽然看起来解决了问题,但是safari已经默认禁用第三方cookie,chrome的无痕模式默认情况下也禁用第三方cookie。有些网站针对禁用提示用户,比如刚刚开始禁用时的淘宝,就是采用提示用户取消禁用的形式来规避问题。很多单点登录也是依靠第三方cookie的,则改为了登陆跳转过程中携带code的方式。具体此处不展开。
其他方案
因为限制第三方cookie是趋势,所以除非不得已还是尽量不要依赖第三方cookie的认证方式,可以选择将跨站资源代理到同站来避免。
但是,如果后端有大量深度不同的全路径资源的接口,如:
css
{
a: {
b: {
c: 'https://cdn.aliyun.com/img.jpg'
}
}
}
此时没办法直接做代理,而且后端和前端replace url,或刷数据的成本都比较高,后续添加接口维护的心智负担也比较重。也可以尝试重定向的方式来作为短期方案。
重定向
此时同一个资源会发送两次:
第一次:重定向
后端判断后重定向到同站地址:storage.taobao.com
此时要注意,因为需要跨域,进行重定向的地址也要添加相应的跨域header,以运行shop.taobao.com访问
第二次:真实请求
第二次访问重定向后的同站地址storage.taobao.com,就可以正常发送cookie了。
此时要注意,chrome对于重定向的请求,会设置origin为null:
那么对应的跨域资源接口的Access-Control-Allow-Origin
需要根据请求来源进行设置,或者设置为null,而不能设置为当前访问页面(shop.taobao.com)
当然如果直接重定向到前端服务器shop.taobao.com,再做代理,可以省略跨域设置。
Service Worker
大量的重定向会多很多无用请求,并不是一个长久之计。对请求资源做拦截会想到service worker,在service worker中替换来达到本地重定向的效果,但是需要注意service worker的安装激活是需要时间的,首次访问的加载时序需要解决,而且需要权衡引入service worker造成的其他相关成本,本文不展开。
SameParty提案
针对第三方cookie的限制本意是保护隐私,但是对正常使用的产品造成了很多不便,所以有了SameParty的提案,在上面正常的业务场景中,所有不同的域名基本上都来自同一个组织或企业,我们希望在同一个运营主体下不同域名的 Cookie
也能共享。
如何定义 First-Party Sets
每一个需要用到 First-Party Sets
策略的域名都应该把一个 JSON
配置托管在 /.well-known/first-party-set
路由下。
例如 shop.taobao.com 的配置应该托管在 https://``shop.taobao.com``/.well-known/first-party-set
下:
json
{
"owner": "taobao.com",
"version": 1,
"members": ["aliyun.com"]
}
另外 aliyun.com 也需要增加所有者的配置:
json
{
"owner": "taobao.com"
}
SameParty设置
ini
Set-Cookie: key=value; Secure; SameSite=Lax; SameParty
- SameParty Cookie 必须包含 Secure.
- SameParty Cookie 不得包含 SameSite=Strict.
Chrome 可以通过开启该选项来试用。
回顾
- 什么时候会发送预检,预检响应需要设置哪些header
- canvas中使用跨域图片如何做认证
- 浏览器什么时候认为请求是cross-site,此时如何携带cookie
- 同名cookie如何确定优先级
- SamePart提案为了解决什么问题