发现问题
最近一年做项目一直都是 CORS 一把梭,非常快乐。
毕竟只要设置 withCredentials,预先请服务端同学加下 CORS 白名单,不管开发环境、测试环境还是线上环境,都可以直接请求。不用配 nginx 转发,不用切换正向代理,不用和测试同学解释配代理的流程。
直到上周五发现有些页面会疯狂刷新,定位到原因是 CAS 登录的问题。进一步定位发现前端发起的跨域请求没有带 Cookie。
然后跟着控制台的警告信息定位到原因:
A cookie associated with a cross-site resource at http://a.com/ was set without the 'SameSite' attribute. A future release of Chrome will only deliver cookies with cross-site requests if they are set with 'SameSite=None' and 'Secure'. You can review cookies in developer tools under Application>Storage>Cookies and see more details at https://www.chromestatus.com/feature/5088147346030592 and https://www.chromestatus.com/feature/5633521622188032.
于是发现是 Chrome 升级 80 导致未设置 SameSite 的 Cookie 默认值是 Lax
,关于 SameSite 各个属性的意思可以参考阮老师博客。而 Lax
定义如下:
问题原因就明朗了。页面疯狂刷新的原因如下:
- 前端发起的跨域请求是 Ajax 请求,因为
SameSite=Lax
,没有带 cookie,服务端认为该用户没有登录,于是返回 401 - 前端发现服务端返回 401,跳转服务端的登录接口:location.href=
${BaseURL}/api/login
- 跳转登录是属于“导航到目标网址的 GET 请求”,会带上 cookie
- 登录接口验证 cookie 是有效用户,于是判断该用户已经登录,再 redirect 回前端页面
- 又回到了1
整个过程陷入了以上的循环往复中,展现出来的结果就是页面一直刷新却不可用。
找到问题后,除了吐槽下 Chrome 升级+吐槽自己没有注意新特性导致陷入被动外,就得找解决方案了。
解决方案
1. 紧急处理
紧急处理其实是在服务方解决问题之前让用户先稍微兼容一下。方案很简单:
-
换个浏览器
or
-
打开 chrome://flags/#same-site-by-default-cookies,关掉
SameSite by default cookies
然后重启浏览器:
2. 方法一:跨域 + Set-Cookie
首先就是报错信息里提到的,让服务端同学在 Set-Cookie 的时候加上 SameSite=None; Secure
。但是我们的后台系统都是对内使用的,很多域名都没有申请证书。而 “Secure” 属性却表示 Cookie 只能随 HTTPS 请求发送。
这就很尴尬。
要不申请证书,要不换域名。
但是长远来看,https是大趋势,趁着这个机会升级成 HTTPS 也是可取的。
然而有些版本的浏览器并不识别 SameSite=None
,需要特殊处理(升级指南看这里)
3. 方法二:改成同域请求
既然 SameSite=Lax
限制的是跨域 Cookie 携带,那么改成同域自然没有问题了。
但因为现在项目都是前后端分离部署的,所以请运维同学配了一下匹配到 ^/api/
开头的请求都转发到服务端的地址。前端请求接口时直接用相对路径请求。
问题解决。
测试环境怎么办?
因为可能会同时做多个需求,会对应不同的测试域名,用方法一(跨域)需要 HTTPS,用方法二(同域)则需要:
-
法1:前端配置 Nginx 反向代理。
这种方式需要把不同的测试地址加入 CAS 验证白名单。每次新开一个提测链接就需要申请,很麻烦;
-
法2:用正向代理,如配置 Chrome 的 SwitchOmega 插件访问单一的测试域名,该域名转发到前端地址,前端再把匹配到服务端路径的请求 proxy_pass 到服务端去。
这又需要测试同学每次测试时候配置代理。前端开发多个项目时也需要频繁切换代理。看起来又回到了快乐的 CORS 之前的蛮荒时代。
-
法3:暴力一些,直接让测试同学关掉 chrome://flags 里的
SameSite by default cookies
,然后跨域访问。听起来不错,是一次性方案,但是这是逆趋势的解决方案,毕竟不是长久之计。
这三种方法感觉都不是很完美。
然后发现有些跨域的项目是没有问题的,有些却不行。其中区别在哪里呢?我本来以为是类似 Chrome 更新 SameSite 时一样的小流量切换,居然忽略了这一现象。
跨站与跨域
上一节产生最后现象的原因在于我没有分清跨站(cross-site)与跨域(cross-origin)。
作为一名前端工程师,我深深地忏悔
我一直忽视了 chrome 文章里说的 “cross-site”,先入为主地以为所有非同源的请求(即跨域的请求)都会受到 SameSite 的限制。
不同项目的呈现结果不一样的原因就清楚了。 出现登录验证问题的项目的前后端二级域名+顶级域名(eTLD+1)是不同的(a1.com 与 a2.com),而没有问题的项目的前后端二级域名+顶级域名却相同(都是 a1.com)。 (这里更正过,之前写的是二级域名。感谢评论区 @zry754331875 同学纠正。)
因此测试环境的问题只需要申请可能出现的二级域名下的子域名作为前端测试环境的地址,然后测试环境使用跨域方式访问就解决了此问题。
总结与反思
虽然很想吐槽 Chrome 升级,但是毕竟是为了防 CSRF 攻击,主要问题在于我自己一直没有注意到这个问题。
-
从 Chrome 的官网来看,77+版本就可以在 chrome://flags 中设置相关属性,而在控制台也应该会有警告信息。但我一直没有注意到警告。直到产品反映+自己遇到才开始定位问题,太滞后了。
-
作为立身于浏览器环境的切图仔,应当随时关注浏览器的新特性才对。但是我一直没有跟进浏览器最新版本的更新内容,此特性从去年 10 月份就已经在一些版本上发布,但我直到今年3月中旬才了解到这一项更新,太滞后了。
-
虽然 Set-Cookie 包括登录逻辑都是后端完成的,但是前端对浏览器和整个过程其实最熟悉。在出现问题后,第一反应是登录验证失败,于是让后端同学去排查问题,浪费了很多时间。
补充概念
eTLD
即 effective top-level domain (有效顶级域)。
Public Suffix List(PSL)
公共后缀列表。
Internet 用户可以(或在历史上可以)直接在公共后缀(public suffix)上注册名称。如 .com, .ci.uk, pvt.k12.ma.us
。
(这里的公共后缀我理解和 eTLD 是一回事)
公共后缀列表是所有已知公共后缀的列表。
公共后缀列表允许浏览器做诸如下面的事情:
- 避免为高级域名后缀设置破坏隐私的 “supercookies”
- 在用户界面中突出显示域名中最重要的部分
- 按站点准确分类历史记录条目
所有list看这里
获取PSL的 npm 包
eTLD+1
有了 eTLD 的概念,eTLD+1 就比较容易理解了。
也就是比 eTLD 再多一级,即一般用户可以注册到的网站域名。(有效顶级域名+二级域名)
扩展阅读
参考资料
Cookies default to SameSite=Lax
Reject insecure SameSite=None cookies
RFC: Same-Site Cookies draft-ietf-httpbis-cookie-same-site-00