如何在最初通过 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 的各种版本,提供了各种共享机制 - 并在后续版本中变得过时。其中包括:
- 缓存,通过虚假端点访问 (ref)
- 一个会话 cookie (ref)
一种不依赖于浏览器共享存储的机制是将服务器生成的令牌添加到 URL (ref), (ref) - 这里的问题是它扰乱了 Android/Chrome,它在 Web 应用程序清单中使用未修改的 start_url
。
这个问题多年来引发了许多 SO 问题(上面提到了其中的三个问题),其中一些问题已经通过显然在 iOS 的早期版本下有效的解决方案得到了解答。我们现在想要的是一个既能在最新版本下工作又能在 Android/Chrome 下工作的解决方案。有优惠吗?
可以做到。以下是我们如何成功做到这一点:
- 当用户最初在浏览器中登录应用程序时,我们会在服务器上生成一个 UID。
- 我们将此 UID 与服务器文件 (
access.data
) 中的用户名配对。
- 我们动态生成网络应用程序清单。在其中我们将
start_url
设置为索引页并附加包含 UID 的查询字符串,例如"start_url": "/<appname>/index.html?accessID=<UID>"
.
- 我们创建一个 cookie 来验证该应用程序是否已被访问,例如
access=granted
.
- 当用户以 iOS PWA 的形式访问应用程序时,应用程序会寻找这个 cookie 但没有找到它(狡猾的;)——我们使用了 iOS 缺陷之一(不是在 Safari 和 PWA 之间共享 cookie)以克服同样的缺陷)。
- 缺少
access
cookie 会告诉应用程序从查询字符串中提取 UID。
- 它将 UID 发送回服务器,服务器在
access.data
中查找匹配项。
- 如果服务器找到匹配项,它会告诉应用程序 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();
我们的要求是让我们的用户通过 URL 登录应用程序,并将该应用程序作为 PWA 添加到他们的主屏幕后,保持登录状态,以便 不需要第二次登录已安装的 PWA。这在 Android/Chrome 下当然是可能的,其中 PWA 可以通过各种机制(包括 cookie、IndexedDB、缓存)初始存储和访问登录状态。
但是,在我们看来,iOS 14/iPadOS 14 下的 PWA 被严格沙盒化,Safari 无法将登录状态传递给它。 多年来,通过 iOS 的各种版本,提供了各种共享机制 - 并在后续版本中变得过时。其中包括:
- 缓存,通过虚假端点访问 (ref)
- 一个会话 cookie (ref)
一种不依赖于浏览器共享存储的机制是将服务器生成的令牌添加到 URL (ref), (ref) - 这里的问题是它扰乱了 Android/Chrome,它在 Web 应用程序清单中使用未修改的 start_url
。
这个问题多年来引发了许多 SO 问题(上面提到了其中的三个问题),其中一些问题已经通过显然在 iOS 的早期版本下有效的解决方案得到了解答。我们现在想要的是一个既能在最新版本下工作又能在 Android/Chrome 下工作的解决方案。有优惠吗?
可以做到。以下是我们如何成功做到这一点:
- 当用户最初在浏览器中登录应用程序时,我们会在服务器上生成一个 UID。
- 我们将此 UID 与服务器文件 (
access.data
) 中的用户名配对。 - 我们动态生成网络应用程序清单。在其中我们将
start_url
设置为索引页并附加包含 UID 的查询字符串,例如"start_url": "/<appname>/index.html?accessID=<UID>"
. - 我们创建一个 cookie 来验证该应用程序是否已被访问,例如
access=granted
. - 当用户以 iOS PWA 的形式访问应用程序时,应用程序会寻找这个 cookie 但没有找到它(狡猾的;)——我们使用了 iOS 缺陷之一(不是在 Safari 和 PWA 之间共享 cookie)以克服同样的缺陷)。
- 缺少
access
cookie 会告诉应用程序从查询字符串中提取 UID。 - 它将 UID 发送回服务器,服务器在
access.data
中查找匹配项。 - 如果服务器找到匹配项,它会告诉应用程序 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();