八股文实战—cors/cross-site/cookie/same-site/same-party

  • canvas中使用跨域图片如何做认证
  • 浏览器什么时候认为请求是cross-site,此时如何携带cookie
  • 同名cookie如何确定优先级
  • SameParty提案为了解决什么问题

跨域、cookie是校招面试高频基础问题。本文用一个模拟案例,希望能让我们在MDN之外,有个实战的理解,为面试出题提供新的素材。

相关内容:跨域跨站, cookieSameSiteSameParty

项目起步

开发一个网站,部署在shop.taobao.com上,所有img等资源和后端接口都通过https://shop.taoba...

同源策略:SOP是同源策略(Same origin policy)的缩写,是一种由美国网景通信公司提出的安全策略。其原理是在Web浏览器中,不同源的文档脚本之间不能相互访问。这种策略可以有效防止跨站脚本攻击。

前后端分离

随着项目推进,前端服务器和后端接口压力不同,不想再用前端服务器反向代理后端接口,开始前后端分离,后端独立部署于api.taobao.com。此时域名不同,导致跨域。此时可以在devtools网络面板中看到请求标头的

Sec-Fetch-Mode: cors,控制台也会有相应报错。

跨域限制

  1. DOM:禁止操作非源页面的DOM与JS对象

这里主要场景是iframe跨域的情况,非同源的iframe是限制互相访问的

  1. XmlHttpRequest:禁止使用XHR对象向不同源的服务器发起HTTP请求,即不能发送跨域ajax请求
  2. 本地存储: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 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。"预检请求"的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。

简单请求需要满足以下几个条件,不满足条件的都是"需预检的请求"

  1. 请求方法
  • get
  • head
  • post
  1. http的头信息不超过以下几种字段
  • Accept
  • Accept-Language
  • Content-Type:(只限于application/x-www-form-urlencoded、multipart/form-data、text/plain)
  • Content-Language
  • Last-Event-ID
  1. 请求中的任意 XMLHttpRequest 对象均没有注册任何事件监听器;XMLHttpRequest 对象可以使用 XMLHttpRequest.upload 属性访问。

  2. 请求中没有使用 ReadableStream 对象

现在大多数接口都是content-type: application/json,那自然就会触发OPTIONS预检。

我们经常使用的常见的复杂请求有:

  1. 请求方法为put或delete

  2. Content-Type字段类型为:application/json

  3. 添加额外的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 主要有以下几个部分组成:

可以看到同域的判断比较严格,需要 protocolhostnameport 三部分完全一致。

跨站判断主要是根据 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.comcdn.taobao.com 就是同站域名

.github.io 是有效顶级域名eTLD,所以 a.github.iob.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 ,而bilibilitoken如果是Strict的话,那我跳转过去就会丢失登录状态。

Domain

Domain 指定了哪些主机可以接受 Cookie。如果不指定,该属性默认为同一 host 设置 cookie,不包含子域名 。如果指定了 Domain,则一般包含子域名。

假设我们在www.taobao.com 页面

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是由DomainPath 来区分的,因此不同的DomainPath 会被识别成不同的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

ExpiresMax-Age属性定义了 Cookie 的生命周期,也就是浏览器应删除 Cookie 的时间。在默认情况下Cookie 的生命周期是 Session 级别,即退出浏览器后自动过期。 与 Http Cache 类似, Expires 是以一个绝对GMT格式的时间的来指定过期时间,而 Max-Age 是以多少秒后过期。Max-Agehttp1.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,或刷数据的成本都比较高,后续添加接口维护的心智负担也比较重。也可以尝试重定向的方式来作为短期方案。

重定向

  1. 申请同站域名:storage.taobao.com
  2. 将来源是shop.taobao.com 的对 cdn.aliyun.com的请求,重定向到https://storage.ta...

此时同一个资源会发送两次:

第一次:重定向

后端判断后重定向到同站地址: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提案为了解决什么问题
相关推荐
齐 飞5 分钟前
MongoDB笔记02-MongoDB基本常用命令
前端·数据库·笔记·后端·mongodb
巧克力小猫猿21 分钟前
基于ant组件库挑选框组件-封装滚动刷新的分页挑选框
前端·javascript·vue.js
FinGet30 分钟前
那总结下来,react就是落后了
前端·react.js
前端李易安30 分钟前
手写一个axios方法
前端·vue.js·axios
于顾而言40 分钟前
【笔记】Go Coding In Go Way
后端·go
2401_8576363940 分钟前
Spring Boot环境下的知识分类与检索
java·spring boot·后端
qq_172805591 小时前
GIN 反向代理功能
后端·golang·go
2401_857026231 小时前
Spring Boot框架下的知识管理与多维分类
spring boot·后端·oracle
XinZong1 小时前
【VSCode插件推荐】想准时下班,你需要codemoss的帮助,分享AI写代码的愉快体验,附详细安装教程
前端·程序员
trim1 小时前
写了个可以在工作中快速摄取知识的神器,都来体验体验
前端·产品