怎么会是跨域的问题

跨域这个问题已经是老生常谈了,我要是看到这两个字,我的第一反应肯定是搞一搞应该马上就解决了,不需要花什么精力在这上面。不过这次还是啪啪打脸,因为确实花了一些时间来解决一个跨域且带 Cookie 的问题。也好久没调试前端相关的内容了,本文做一个简单的记录,因为过程实在是挺有意思的。

我的使用场景

我碰到的问题,场景是 Chrome 浏览器插件内,跨域带 Cookie。首先,这是一个远古项目了,很多东西传了一代又一代,到我这时,代码已经十分“美妙”。浏览器插件加载 Background 时,引入了一个上传到阿里云 OSS 的 JS 文件,JS 文件中调用了后端一些接口,Ajax 的形式,或者 iframe 再嵌入一个后端链接直接渲染,而后端的实现是传统的 Session 登录态,前端调用的接口都是有状态的,如果没有登录后台系统,会重定向到一个登录服务链接(不支持跨域)。那么这个实现就要求我们必须跨域时必须带上 Cookie。听起来有点麻烦,感觉挺难搞的。但是你思考一下,好像也简单。

首先是前端跨域带 Cookie,我们的常规做法,JQuery 这里:

$.ajax({ type:'get', crossDomain:true, // 设置允许跨域 xhrFields: { withCredentials: true // 设置带上 cookie }, }})

我们只需要加上 crossDomain 和 withCredentials 就行了。

后端,比如 PHP 中,通常我们会设置:

header("Access-Control-Allow-Origin: *");

但是这里不行。我们为了搭配前端的 withCredentials ,需要设置 Access-Control-Allow-Credentials 以及 Access-Control-Allow-Headers  不能为 *,例如:

header("Access-Control-Allow-Headers: *"); header("Access-Control-Allow-Credentials: true"); header("Access-Control-Allow-Methods: POST,GET,OPTIONS"); header("Cache-Control: no-cache"); header("Content-Type: application/json"); $origin = isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : ''; $allowOrigin = [ 'https://item.taobao.com', 'https://detail.tmall.com' ]; if (in_array($origin, $allowOrigin)) { header("Access-Control-Allow-Origin:" . $origin); } else { header("Access-Control-Allow-Origin: *"); }

那么,这个时候我们应该就能跨域且带 Cookie 了。

Emmmm~~显然没那么简单,穿梭在各种开发测试环境中 Debug 的过程简直是折磨,特别是对于一个后端开发来说,但是照着红字报错 Debug 总没错。

Chrome 80 SameSite 属性

然后我就发现了 Chrome 80 的改动,很久没写前端了,确实没关注。

在 Chrome 80 中,Chrome 的 SameSite 会有这样的变动:

  • 默认情况下,所有未指定 SameSite 属性的 cookie 将自动强制设置为 SameSite=Lax
  • SameSite=None 的 Cookie 必须安全,就是说只有采用 SameSite=None; Secure 设置的 cookie 可以从外部通过 HTTPS 访问

查了一下 SameSite 可以有三个不同的值:Strict、Lax 或 None。

描述
Strict 只有在访问最初设置的域时,才可访问具有此设置的 Cookie。换言之,Strict 会完全阻止跨站点使用 Cookie。这种选择最适用于需要高安全性的应用,如银行。
Lax 具有此设置的Cookie仅在同一站点请求或具有非幂等HTTP请求的顶级导航上发送,如 HTTP GET。 因此,如果第三方可以使用 cookie,但增加了安全优势,保护用户免受CSRF攻击的侵害,则将使用此选项。
None 使用此设置的 Cookie 将像 Cookie 之前工作的方式一样工作。

那么,这个规则一出来,肯定会影响一些以前的应用。

我找到一个表格,非常有意思,SameSite 在不同的应用场景对比新默认策略和之前的不同之处:

请求类型 示例 SameSite=None SameSite=Lax
链接 <a href="…"></a> 发送 Cookie 发送 Cookie
预加载 <link rel=“prerender” href="…"/> 发送 Cookie 发送 Cookie
GET 表单 <form method=“GET” action="…"> 发送 Cookie 发送 Cookie
POST 表单 <form method=“POST” action="…"> 发送 Cookie 不发送
iframe <iframe src="…"></iframe> 发送 Cookie 不发送
AJAX $.get("…") 发送 Cookie 不发送
Image <img src="…"> 发送 Cookie 不发送

很明显,iframe 和 Ajax,已经不能携带 Cookie 了,即使你做了一波上面的操作。那么这个时候我们要怎么做呢?

解决方案

查了一些资料,什么修改浏览器配置这些就不要看了,不是解决问题的办法。要解决这个问题,似乎也很简单。

首先修改 Cookie 的设置,需要设置 SameSite=None; Secure,具体的怎么设置,查一下你使用的框架就好,如果是原生 PHP,PHP 官方也有 SameSite 的说明。

这里举一个 Yii 的例子:

$session = [ 'class' => 'yii\redis\Session', 'timeout' => 3600 * 16, 'cookieParams' => [ 'httponly' => true, 'secure' => true, 'path' => '/', 'sameSite' => "None", ], ];

Component 中看情况设置 secure 和 sameSite 就行。

后端 Cookie 设置完,前端用上 HTTPS,这里要注意的是,需要是你调用接口的脚本也得是 HTTPS。举几个例子,关注一下中间那个环节:

  • 源站点 -> 加载 JS 文件(HTTPS) -> JS 中调用 Ajax
  • 源站点 -> Get 后端接口(HTTPS)返回 IFrame 的 HTML -> IFrame 中链接另一个后端渲染页面(HTTPS)
  • 源站点 -> 加载 CDN 中的 JS 文件 (HTTPS)-> JS 中调用 Ajax

在设置了 secure 的 Cookie 后,我们要带上 Cookie 就必须上 HTTPS,否则就带不过去,而且会很迷惑地看似应该能带过去实则没带。Cookie 带不过去,登录态没获取到,就重定向跳转了。其实 IFrame 的情况反而更好解决,因为在中间过程中不是跨域了。

小提示

这里给一些过程中的小提示。

在调试的过程中。我们需要用到本地 HTTPS 去调试,否则很麻烦。推荐一个生成本地自签证书的小工具 mkcert,基本上就是一键 HTTPS 了,挺好用。

用以下命令:

mkcert -install

就能生本地 CA 了,拿着证书就能部署了。不管是 Nginx Server 还是其它,都能用。

顺便提一下 VS Code 的 Live Server 插件,一键 server 非常好用,配置如下:

{ "liveServer.settings.https": { "enable": true, //set it true to enable the feature. "cert": "~/primary.crt", //full path of the certificate "key": "~/private.key", //full path of the private key "passphrase": "12345" } }

还有一个提示,在本地开发的过程中,非常容易忽略,配置的本地 HOST 会给你造成调通了的假象。比如。我本地直接 serve 我的 JS 进行调试(这样好调试),那么我的 JS 会是 https://127.0.0.1/..../global.js  这样的路径,也许你还会配置了一个本地的虚拟域名,比如 https://baidu.taobao ,指向本地 127.0.0.1,然后你就开始在 JS 和后端之间进行调试了,调通了,上线了,崩了!为什么呢?本地虚拟域名,还是 127.0.0.1,所以调用过程中,没有跨域哦,调通了实际上不一定就通了。

总结

Token 验证不香吗?搁这整啥 Cookie 呢。