Safari 不使用 JS Fetch 设置 CORS cookie API

Safari not setting CORS cookies using JS Fetch API

在使用 Fetch API(实际上是通过 fetch polyfill)时,我无法让 Safari 从服务器响应中成功应用 Set-Cookie。相同的代码在 FF 和 Chrome 中正常工作(我使用本机和 polyfill fetch 进行了测试)。

  1. 请求跨域;
  2. 是的,我正在设置 credentials: true;
  3. 服务器确实以 Set-Cookie header;
  4. 响应
  5. 后续请求从 Chrome 和 FF 发送 cookie 请求 headers,但 Safari 不会;
  6. 请求使用 HTTPS(证书是 self-signed 并且在开发域上,但它似乎在常规请求中被 Safari 接受);和

有人知道问题出在哪里吗?

我通读了文档并经历了许多 closed bug reports. Unless I missed something, I think maybe the problem is with the 'default browser behaviour' 处理 cookie 和 CORS —— 而不是 fetch(通读了 polyfill 源代码,似乎 100% 不知道 cookie) .一些错误报告表明格式错误的服务器响应可能会阻止保存 cookie。

我的代码如下所示:

function buildFetch(url, init={}) {
    let headers = Object.assign({}, init.headers || {}, {'Content-Type': 'application/json'});
    let params = Object.assign({}, init, { credentials: 'include', headers });

    return fetch(`${baseUrl}${url}`, params);
}

buildFetch('/remote/connect', {method: 'PUT', body: JSON.stringify({ code })})
.then(response => response.json())
.then(/* complete authentication */)

实际授权请求如下。我正在使用 cURL 获取准确的 request/response 数据,因为 Safari 很难 copy/paste 它。

curl 'https://mydevserver:8443/api/v1/remote/connect' \
-v \
-XPUT \
-H 'Content-Type: application/json' \
-H 'Referer: http://localhost:3002/' \
-H 'Origin: http://localhost:3002' \
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/602.4.8 (KHTML, like Gecko) Version/10.0.3 Safari/602.4.8' \
--data-binary '{"token":"value"}'


*   Trying 127.0.0.1...
* Connected to mydevserver (127.0.0.1) port 8443 (#0)
* TLS 1.2 connection using TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256
* Server certificate: mydevserver
> PUT /api/v1/remote/connect HTTP/1.1
> Host: mydevserver:8443
> Accept: */*
> Content-Type: application/json
> Referer: http://localhost:3002/
> Origin: http://localhost:3002
> User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/602.4.8 (KHTML, like Gecko) Version/10.0.3 Safari/602.4.8
> Content-Length: 15
> 
* upload completely sent off: 15 out of 15 bytes
< HTTP/1.1 200 OK
< Access-Control-Allow-Origin: http://localhost:3002
< Access-Control-Allow-Credentials: true
< Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Api-Key, Device-Key
< Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
< Access-Control-Expose-Headers: Date
< Content-Type: application/json; charset=utf-8
< Content-Length: 37
< Set-Cookie: express:sess=[SESSIONKEY]=; path=/; expires=Fri, 17 Feb 2017 15:30:01 GMT; secure; httponly
< Set-Cookie: express:sess.sig=[SIGNATURE]; path=/; expires=Fri, 17 Feb 2017 15:30:01 GMT; secure; httponly
< Date: Fri, 17 Feb 2017 14:30:01 GMT
< Connection: keep-alive
< 
* Connection #0 to host mydevserver left intact
{"some":"normal","response":"payload"}

回答我自己的问题。

虽然我理解他们的动机,但我发现这是 Safari 的 "working as intended" 行为,这让我非常愤怒。 XHR(大概是本地登陆时的本地获取)根本不支持 third-party cookies 的设置。这个失败是完全透明的,因为它是由浏览器在脚本上下文之外处理的,所以 client-based 解决方案实际上是不可能的。

您会在此处找到一个推荐的解决方案,即打开 window 或 iframe 到 API 服务器上的 HTML 页面,并在那里设置 cookie。此时,第 3 方 cookie 将开始工作。这非常糟糕,并且不能保证 Safari 不会在某个时候关闭该漏洞。

我的解决方案基本上是重新实现一个身份验证系统来执行 session-cookies 所做的事情。即:

  1. 添加一个新的 header、X-Auth: [token],其中 [token] 是一个非常小的 short-lived JWT,其中包含您 session 所需的信息(理想情况下只有用户 ID——在应用程序的生命周期内不太可能发生变化的东西——但如果权限可以在 session);
  2. 期间更改,则绝对不是权限之类的东西
  3. X-Auth 添加到 Access-Control-Allow-Headers;
  4. 在 sign-in 期间,使用您需要的有效负载设置 session cookie 和身份验证令牌(Safari 和 non-Safari 用户将同时获得 cookie 和身份验证 header);
  5. 在客户端上,查找 X-Token 响应 header 并在任何时候将其作为 X-Token 请求 header 回显(您可以实现持久化)通过使用本地存储——令牌会过期,因此即使价值存在多年,也无法在某个时间点后赎回);
  6. 在服务器上,对于受保护资源的所有请求,检查 cookie,如果存在则使用它;
  7. 否则(如果 cookie 不存在——因为 Safari 没有发送它),查找 header 令牌,验证并解码令牌负载,更新当前的 session提供信息,然后生成一个新的身份验证令牌并将其添加到响应 headers;
  8. 照常进行。

请注意,JWT(或任何类似的东西)旨在解决一个完全不同的问题,并且由于 "replay" 问题,真的不应该用于 session 管理(想想如果用户有两个 windows 用他们自己的 header-state 打开)。然而,在这种情况下,它们提供了您通常需要的短暂性和安全性。底线是您应该在支持它们的浏览器上使用 cookie,保持 session 信息尽可能小,尽可能保持您的 JWT short-lived,并构建您的服务器应用程序以预期意外和恶意重播攻击。

仅供参考,大约 18 个月后尝试这个,这个解决方案对我不起作用。或者,对于某些用户来说,它似乎是间歇性的,这真的很奇怪。

One recommended solution you will find here is to open a window or iframe to an HTML page on the API server and set a cookie there. At this point, 3rd party cookies will begin to work. This is pretty fugly and there is no guarantee that Safari won't at some point close that loophole.

最好的猜测是,无论 Safari 在内部使用什么逻辑,都取决于您设法获取 cookie 的顺序,或者更复杂和不透明的东西。

我最终确定的解决方案是,如果您因为从与 API 不同的主机提供 React 应用程序或以其他方式控制这两个站点而遇到此问题,这可能是一个选择使用 DNS:

我们的客户在 www.company-name.com 接受服务,而我们的 API 在公司-name.herokuapp.com。通过创建 CNAME 记录 api.company-name.com --> company-name.herokuapp.com,以及将同一域的子域用于从客户端到 API 的请求,Safari 不再将其视为 "third-party" cookie。

优点是涉及的代码很少,而且都使用了成熟的东西...缺点是如果您需要 API 主机,则需要一些 control/ownership将使用 https - 他们需要一个对客户端域有效的证书,否则用户将收到证书警告 - 所以如果 API 在问题不是你或合作伙伴的。