前端/后端分离: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 客户端不同的域上的主要内容,如果未能解决你的问题,请参考以下文章

前后端分离微服务架构如何设计

前后端分离,如何在前端项目中动态插入后端API基地址?(in docker)

API 调用与来自前端的直接页面调用(AJAX)

前后端分离的好处有哪些?

前后端分离实践 — 如何解决跨域问题

前端完全分离和前端不完全分离