凭证管理让 PWA 用户保持登录状态,而不只是登录表单
跨安装、浏览器会话和设备切换的持久身份是让 PWAs 感觉像已安装应用的关键。
为什么身份连续性对已安装的 web 应用很重要
当用户安装 web 应用时,他们期望应用记住他们。他们不想每次打开时都重新身份验证。但 PWAs 在浏览器中运行,浏览器有自己的会话管理、cookie 策略和安全约束,如果处理不当,可能会中断用户身份。
每隔几天就丢失身份验证的 PWA 感觉像是坏了。具有无缝登录的安装的 web 应用感觉像是原生的。
PWA 身份验证栈
你的身份策略应该分层这些组件:
用户操作
↓
凭证 UI(凭证管理 API)
↓
身份验证 API(WebAuthn / OAuth / 密码)
↓
会话令牌(JWT / cookie)
↓
Service Worker 缓存(离线会话验证)
↓
后台同步(在线时令牌刷新)每一层处理不同的用例。如果一层失败,下面的层无法补偿。
凭证管理 API:原生登录 UI
凭证管理 API 用你可以控制的编程式界面替换了浏览器旧的“保存密码”对话框。
基本凭证存储
// 成功登录后
navigator.credentials.store({
id: userId,
password: plainPassword, // 仅用于密码身份验证
name: userDisplayName,
iconURL: avatarUrl
});联合凭证(Google、Facebook 等)
navigator.credentials.store({
id: googleUserId,
type: 'federated',
provider: 'https://accounts.google.com',
name: userDisplayName,
iconURL: avatarUrl
});公钥凭证(WebAuthn / passkeys)
navigator.credentials.store({
id: credentialId,
type: 'public-key',
transports: ['internal', 'hybrid'],
name: userDisplayName,
iconURL: avatarUrl
});应用加载时检索
navigator.credentials.get({
password: true,
federated: {
providers: ['https://accounts.google.com']
}
}).then(credential => {
if (credential) {
// 自动登录或提示用户确认
authenticateWithCredential(credential);
}
});Service worker 会话持久化
当用户退出浏览器时,浏览器可能会清除会话 cookie。Service workers 为会话验证提供更持久的存储。
会话缓存策略
// service-worker.js
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('/api/auth/validate')) {
event.respondWith(
caches.open('auth-session').then(cache =>
cache.match('current-session').then(cachedResponse => {
if (cachedResponse) {
// 使用后端验证缓存会话
return fetch('/api/auth/validate', {
headers: { 'X-Auth-Token': cachedResponse.headers.get('X-Auth-Token') }
}).then(response => {
if (response.ok) {
cache.put('current-session', response.clone());
return response;
} else {
cache.delete('current-session');
return fetch(event.request);
}
});
}
return fetch(event.request).then(response => {
if (response.ok) {
cache.put('current-session', response.clone());
}
return response;
});
})
)
);
}
});离线身份处理
当你的 PWA 离线时,缓存的凭证将不起作用。优雅地处理此情况:
// service-worker.js
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('/api/') && !navigator.onLine) {
event.respondWith(
caches.match(event.request).then(response => {
if (response) {
return response;
}
// 为依赖身份验证的路由返回离线响应
return new Response(JSON.stringify({
error: 'offline',
message: '请连接互联网以登录'
}), {
status: 503,
headers: new Headers({ 'Content-Type': 'application/json' })
});
})
);
}
});使用后台同步进行令牌刷新
会话令牌会过期。后台同步静默刷新它们,以便用户不会注意到。
// service-worker.js
self.addEventListener('sync', (event) => {
if (event.tag === 'token-refresh') {
event.waitUntil(
fetch('/api/auth/refresh')
.then(response => response.json())
.then(data => {
// 使用新令牌更新存储的凭证
caches.open('auth-session').then(cache =>
cache.put('current-session', new Response(JSON.stringify(data), {
headers: { 'Content-Type': 'application/json' }
}))
);
})
.catch(error => console.error('令牌刷新失败:', error))
);
}
});
// 在成功身份验证后注册同步
navigator.serviceWorker.ready.then(registration => {
registration.sync.register('token-refresh');
});WebAuthn 集成模式
WebAuthn (FIDO2) 支持使用设备绑定凭证进行无密码身份验证。这对 PWAs 来说是黄金标准,因为它绑定到设备而不是浏览器会话。
注册
const publicKeyCredentialCreationOptions = {
challenge: new Uint8Array(32),
rp: {
name: '你的应用',
id: window.location.hostname
},
user: {
id: new Uint8Array(16),
name: 'user@example.com',
displayName: '张三'
},
pubKeyCredParams: [{ type: 'public-key', alg: -7 }],
authenticatorSelection: {
authenticatorAttachment: 'platform',
userVerification: 'preferred'
}
};
navigator.credentials.create({
publicKey: publicKeyCredentialCreationOptions
}).then(credential => {
// 将凭证发送到后端进行注册
registerPublicKeyCredential(credential);
});身份验证
const publicKeyCredentialRequestOptions = {
challenge: new Uint8Array(32),
rpId: window.location.hostname,
allowCredentials: [],
userVerification: 'preferred'
};
navigator.credentials.get({
publicKey: publicKeyCredentialRequestOptions
}).then(assertion => {
// 将断言发送到后端进行验证
authenticateWithPublicKeyCredential(assertion);
});实用实施检查清单
在发布身份验证之前,验证:
- [ ] 凭证管理 API 用于密码和联合登录
- [ ] 提供 WebAuthn 作为选项(无密码 / passkeys)
- [ ] Service worker 缓存会话令牌以进行离线验证
- [ ] 后台同步静默处理令牌刷新
- [ ] 清晰的注销流程清除凭证、缓存和 service worker 存储
- [ ] 向用户传达会话过期(不只是静默注销)
- [ ] 跨设备身份同步已记录(例如,密码管理器、云同步)
安装之间的身份连续性
当用户在多个设备上安装你的 PWA 时,他们期望身份跟随他们。
场景 1:用户在新设备上安装 PWA
- 浏览器提供从密码管理器导入凭证
- 你的应用通过电子邮件或用户 ID 识别现有用户账户
- 无需重新身份验证的无缝登录
场景 2:用户在同一设备上切换浏览器
- 存储在凭证管理 API 中的凭证是浏览器作用域
- 提供密码导出/导入或使用密码管理器同步
- WebAuthn 凭证是设备作用域的,不传输
场景 3:用户重新安装 PWA
- 重新安装时清除 Service worker 缓存
- 凭证管理 API 持久化(除非用户清除站点数据)
- WebAuthn 凭证持久化(设备绑定)
要避免的常见身份验证反模式
反模式 1:内存中会话令牌
// ❌ 不良:在导航/刷新时丢失
let sessionToken;
function login(creds) {
sessionToken = authenticate(creds);
}// ✅ 良好:存储在 service worker 缓存中
function login(creds) {
return authenticate(creds).then(token => {
caches.open('auth-session').then(cache =>
cache.put('current-session', new Response(token))
);
});
}反模式 2:静默注销没有反馈
// ❌ 不良:用户在功能中断时发现注销
if (tokenExpired) {
clearCredentials();
}// ✅ 良好:通知用户并引导到登录
if (tokenExpired) {
showNotification('会话已过期。请重新登录。');
clearCredentials();
navigateToLogin();
}反模式 3:没有离线登录指导
// ❌ 不良:离线用户看到神秘的身份验证错误
fetch('/api/auth/login')
.catch(error => showError('身份验证错误'));// ✅ 良好:清晰的离线消息
if (!navigator.onLine) {
showNotification('请连接互联网以登录');
showOfflineLoginUI();
}来源
- Credential Management API
- WebAuthn API
- Service Worker Cache API
- Background Sync API
- OAuth 2.0 for Browser-Based Apps
下一步
分阶段实施身份连续性:
- 用凭证管理 API 替换传统登录表单
- 添加 WebAuthn (passkeys) 作为身份验证选项
- 实施 service worker 会话缓存
- 添加后台同步进行令牌刷新
- 测试跨安装、浏览器和设备的身份验证流程
你的 PWA 应该像原生应用一样持久和个性化。无缝登录是安装用户的关键信任信号。