如何在最初通过 Safari 14/iOS 14 加载的 PWA 中保持登录状态?

How to maintain login status in a PWA initially loaded via Safari 14/iOS 14?

我们的要求是让我们的用户通过 URL 登录应用程序,并将该应用程序作为 PWA 添加到他们的主屏幕后,保持登录状态,以便 不需要第二次登录已安装的 PWA。这在 Android/Chrome 下当然是可能的,其中 PWA 可以通过各种机制(包括 cookie、IndexedDB、缓存)初始存储和访问登录状态。

但是,在我们看来,iOS 14/iPadOS 14 下的 PWA 被严格沙盒化,Safari 无法将登录状态传递给它。 多年来,通过 iOS 的各种版本,提供了各种共享机制 - 并在后续版本中变得过时。其中包括:

  1. 缓存,通过虚假端点访问 (ref)
  2. 一个会话 cookie (ref)

一种不依赖于浏览器共享存储的机制是将服务器生成的令牌添加到 URL (ref), (ref) - 这里的问题是它扰乱了 Android/Chrome,它在 Web 应用程序清单中使用未修改的 start_url

这个问题多年来引发了许多 SO 问题(上面提到了其中的三个问题),其中一些问题已经通过显然在 iOS 的早期版本下有效的解决方案得到了解答。我们现在想要的是一个既能在最新版本下工作又能在 Android/Chrome 下工作的解决方案。有优惠吗?

可以做到。以下是我们如何成功做到这一点:

  1. 当用户最初在浏览器中登录应用程序时,我们会在服务器上生成一个 UID。
  2. 我们将此 UID 与服务器文件 (access.data) 中的用户名配对。
  3. 我们动态生成网络应用程序清单。在其中我们将 start_url 设置为索引页并附加包含 UID 的查询字符串,例如"start_url": "/<appname>/index.html?accessID=<UID>".
  4. 我们创建一个 cookie 来验证该应用程序是否已被访问,例如access=granted.
  5. 当用户以 iOS PWA 的形式访问应用程序时,应用程序会寻找这个 cookie 但没有找到它(狡猾的;)——我们使用了 iOS 缺陷之一(不是在 Safari 和 PWA 之间共享 cookie)以克服同样的缺陷)。
  6. 缺少 access cookie 会告诉应用程序从查询字符串中提取 UID。
  7. 它将 UID 发送回服务器,服务器在 access.data 中查找匹配项。
  8. 如果服务器找到匹配项,它会告诉应用程序 PWA 用户已经登录,无需再次显示登录屏幕。任务完成!

注意:Android/Chrome 只是忽略了查询字符串中的 accessID - 我在问题中错误地暗示 Android/Chrome 需要未修改的 start_url.

生成 Webapp 清单和更改 start_url 有其自身的后果。

例如,有时我们想要传递的数据不能立即使用,而且如果数据传入 url 我们应该确保传递的登录数据在第一次打开 Webapp 后无效,否则共享书签会还共享用户的登录凭据。 这样做你会失去 start_url 的力量,这意味着如果用户在你的网站处于 subdirectory1 时添加它,它总是会在 subdirectory1 之后打开。

还有什么选择?

从 ios 14 开始,safari shares cacheStorage with Webapps. 因此开发人员可以将凭据作为缓存保存在 cacheStorage 中,并在 Webapp 中访问它们。

代码兼容性

关于 ios14 的可用性我们应该考虑在 ios14 之前超过 90% 的用户已经更新到 ios13 而事实上 ios14 是所有支持 ios 13 的设备都支持,我们可以假设 ios 14 的使用率很快就会达到 90%+,而另外 ~5% 的人总是可以在 Webapp 中再次登录。 根据statcounter

,它已经在 12 天内达到 28%

代码示例

这是我在 Web 应用程序中使用的代码,它可以成功地与 ios 添加到主屏幕。

    ///change example.com with your own domain or relative path.
    function createCookie(name, value, days) {
      if (days) {
        var date = new Date();
        date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
        var expires = "; expires=" + date.toGMTString();
      } else var expires = "";
      document.cookie =
        name + "=" + value + expires + "; path=/; domain=.example.com";
    }
    
    function readCookie(name) {
      var nameEQ = name + "=";
      var ca = document.cookie.split(";");
      for (var i = 0; i < ca.length; i++) {
        var c = ca[i];
        while (c.charAt(0) == " ") c = c.substring(1, c.length);
        if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
      }
      return undefined;
    }
    
    
    async function setAuthFromCookie() {
      caches.open("auth").then(function (cache) {
        console.log("Set Cookie");
        return cache.add(["https://example.com/cacheAuth.php"]);
      });
    }
    async function setAuthToCookie() {
      var uid = readCookie("uid");
      var authKey = readCookie("authKey");
      caches.open("auth").then((cache) => {
        cache
          .match("https://example.com/cacheAuth.php", {
            ignoreSearch: true,
          })
          .then((response) => response.json())
          .then((body) => {
            if (body.uid && uid == "undefined") {
              /// and if cookie is empty
              console.log(body.authKey);
              createCookie("authKey", body.authKey, 3000);
            }
          })
          .catch((err) => {
            console.log("Not cached yet");
          });
      });
    }

    setTimeout(() => {
      setAuthFromCookie();
      //this is for setting cookie from server
    }, 1000);

    setAuthToCookie();