前端/后端分离:Safari 不存储来自 API 的 cookie,该 API 托管在与其前端 SPA 客户端不同的域上
Posted
技术标签:
【中文标题】前端/后端分离:Safari 不存储来自 API 的 cookie,该 API 托管在与其前端 SPA 客户端不同的域上【英文标题】:Frontend/Backend separation: Safari not storing cookies from API which is hosted on a separate domain than its Frontend SPA client 【发布时间】:2019-02-26 01:08:12 【问题描述】:我有一个设置 - 据我所知 - 现在相当普遍:一个后端 REST API 位于其自己的域中,例如 myapi.com
,以及一个在其他地方提供服务的单页前端应用程序,例如myapp.com
。
SPA 是 API 的客户端,API 要求用户在执行操作之前进行身份验证。
后端 API 使用 cookie 来存储一些允许来源的会话数据,其中包括myapp.com
。这是为了有一个安全的总线来传输和存储身份验证数据,而不必担心客户端。
在 Chrome、Opera 和 Firefox 中,这很好用:调用 API 来验证用户身份,返回 Cookie 并将其存储在浏览器中,以便与下一次调用一起推送。
另一方面,Safari 确实收到了 cookie 但拒绝存储它们:
我怀疑 Safari 将 API 域视为第 3 方 cookie 域,因此阻止存储 cookie。
这是 Safari 中的预期行为吗?如果是这样,有哪些最佳做法可以绕过它?
【问题讨论】:
嗨,我遇到了这个确切的问题,使用 express-session。您是否偶然获得了使用 javascript/express 的公认解决方案? 【参考方案1】:对我有用的方法(正如其他人/在其他类似问题中所提到的)是将我的前端和后端放在同一个域下,例如:
frontend.myapp.com backend.myapp.com
然后,Mac Monterey 上的 Safari 和 ios 15 上的 Safari 开始允许来自 backend.myapp.com 的set-cookie
(使用 Secure、HttpOnly、SameSite=none)并从 frontend.myapp.com 访问它们
【讨论】:
【参考方案2】:在这个上延续answering your own question 的传统。
TL;DR 这是 Safari 中所需的行为。解决它的唯一方法是将用户带到托管在 API 域上的网页(问题中的myapi.com
)并从那里设置一个 cookie - 真的,如果你愿意,你可以在 cookie 中写一首小诗.
完成此操作后,该域将被“列入白名单”,Safari 将对您很好,并在任何后续调用中设置您的 cookie,即使来自不同域的客户端。
这意味着您可以保持身份验证逻辑不变,只需引入一个将为您设置“种子”cookie 的哑端点。在我的 Ruby 应用程序中,如下所示:
class ServiceController < ActionController::Base
def seed_cookie
cookies[:s] = value: 42, expires: 1.week, httponly: true # value can be anything at all
render plain: "Checking your browser"
end
end
客户端,您可能需要检查if the browser making the request is Safari 并在打开那个丑陋的弹出窗口后推迟您的登录逻辑:
const doLogin = () =>
if(/^((?!chrome|android).)*safari/i.test(navigator.userAgent))
const seedCookie = window.open(`http://myapi.com/seed_cookie`, "s", "width=1, height=1, bottom=0, left=0, toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no")
setTimeout(() =>
seedCookie.close();
// your login logic;
, 500);
else
// your login logic;
更新:上述解决方案适用于登录用户,即它正确地将当前浏览器会话的 API 域“列入白名单”。
但不幸的是,用户刷新页面似乎会使浏览器重置为 API 域的第 3 方 cookie 被阻止的原始状态。
我发现处理窗口刷新情况的一种好方法是在页面加载时 detect it in javascript 并将用户重定向到与上述相同的 API 端点,然后将用户重定向到原始他们正在导航到的 URL(正在刷新的页面):
if(performance.navigation.type == 1 && /^((?!chrome|android).)*safari/i.test(navigator.userAgent))
window.location.replace(`http://myapi.com/redirect_me`);
更复杂的是,如果响应的 HTTP 状态是 30X(重定向),Safari 将不会存储 cookie。因此,Safari 友好的解决方案包括设置 cookie 并返回 200 响应以及 JS sn-p,该 JS sn-p 将在浏览器中处理重定向。
在我的例子中,作为 Rails 应用程序的后端,这个端点是这样的:
def redirect_me
cookies[:s] = value: 42, expires: 1.week, httponly: true
render body: "<html><head><script>window.location.replace('#request.referer');</script></head></html>", status: 200, content_type: 'text/html'
end
【讨论】:
不仅适用于 Safari(默认情况下会阻止 3rd 方 cookie)。 Chrome 也有此设置,启用后会阻止所有第 3 方 cookie。 Chrome 可以正常工作@Ioanna,如果您使用带有安全和 httponly 标志的 sameSite 标志(例如: httpOnly: true, secure: true, sameSite: 'none')。但是,您发布的这个解决方案似乎很痛苦 coconup?在这种情况下,这是让 safari 存储 cookie 的唯一方法,这似乎很疯狂?我原以为 cors 来源标志和 cookie 域标志可以充分保证 cookie 是合法的 我试过了,但没有运气。我不知道这是否是 SameSite/Secure 配置的问题,或者这个技巧是否不再可能。到 2021 年,这对您仍然有效吗?以上是关于前端/后端分离:Safari 不存储来自 API 的 cookie,该 API 托管在与其前端 SPA 客户端不同的域上的主要内容,如果未能解决你的问题,请参考以下文章