如何在Firebase中限制用户仅拥有一个活跃认证会话?
实现Firebase用户单设备登录的最优方案
方案一:Admin SDK + 会话时间戳控制(推荐)
核心思路:通过Firebase Admin SDK设置用户的tokensValidAfterTime字段,让所有早于当前登录时间的旧会话刷新令牌失效,仅保留当前登录的会话。
- React客户端登录后调用云函数传递用户信息
import { getAuth, signInWithEmailAndPassword } from "firebase/auth"; const auth = getAuth(); signInWithEmailAndPassword(auth, email, password) .then(async (userCredential) => { const user = userCredential.user; const loginTime = new Date().toISOString(); // 调用云函数触发旧会话失效逻辑 const revokeOldSessions = window.functions.httpsCallable('revokeOldSessions'); await revokeOldSessions({ uid: user.uid, loginTime }); }) .catch(console.error);
- 云函数中用Admin SDK更新用户会话有效期
const functions = require("firebase-functions"); const admin = require("firebase-admin"); admin.initializeApp(); exports.revokeOldSessions = functions.https.onCall(async (data) => { const { uid, loginTime } = data; try { // 设置所有早于登录时间的令牌失效 await admin.auth().updateUser(uid, { tokensValidAfterTime: new Date(loginTime) }); return { success: true }; } catch (error) { throw new functions.https.HttpsError('internal', 'Failed to revoke old sessions', error); } });
旧设备的刷新令牌会因早于有效期而无法获取新ID Token,过期后自动退出;当前设备会话不受影响。
方案二:登录事件触发器 + 强制撤销所有旧会话(简单直接)
监听用户登录事件,触发撤销该用户所有会话,当前设备需重新登录(适合对无缝体验要求不高的场景)。
- 云函数监听登录事件
exports.onUserSignIn = functions.auth.user().onSignIn(async (user) => { // 撤销该用户所有刷新令牌 await admin.auth().revokeRefreshTokens(user.uid); const userRecord = await admin.auth().getUser(user.uid); console.log(`Revoked all tokens for user ${user.uid}, valid after: ${userRecord.tokensValidAfterTime}`); });
- React客户端监听令牌失效自动重新登录
import { getAuth, onIdTokenChanged, signInWithEmailAndPassword } from "firebase/auth"; const auth = getAuth(); onIdTokenChanged(auth, async (user) => { if (user) { const idTokenResult = await user.getIdTokenResult(); // 令牌失效时触发重新登录(需提前存储用户密码或使用其他免密登录方式) if (!idTokenResult.claims.auth_time) { await signInWithEmailAndPassword(auth, user.email, localStorage.getItem('savedPassword')); } } });
方案三:自定义Claims记录活跃会话(细粒度控制)
通过自定义Claims记录当前活跃的设备标识和令牌哈希,客户端每次请求时验证会话合法性,适合需要精准控制设备的场景。
- React客户端登录时生成设备标识并调用云函数
import { getAuth, signInWithEmailAndPassword } from "firebase/auth"; const crypto = require('crypto'); const auth = getAuth(); let deviceId = localStorage.getItem('deviceId'); if (!deviceId) { deviceId = crypto.randomUUID(); localStorage.setItem('deviceId', deviceId); } signInWithEmailAndPassword(auth, email, password) .then(async (user) => { const refreshToken = user.refreshToken; const tokenHash = crypto.createHash('sha256').update(refreshToken).digest('hex'); const setActiveSession = window.functions.httpsCallable('setActiveSession'); await setActiveSession({ uid: user.uid, deviceId, tokenHash }); });
- 云函数设置自定义Claim
exports.setActiveSession = functions.https.onCall(async (data) => { const { uid, deviceId, tokenHash } = data; await admin.auth().setCustomUserClaims(uid, { activeSession: { deviceId, tokenHash } }); return { success: true }; });
- 客户端请求前验证会话合法性
import { getAuth, signOut } from "firebase/auth"; const crypto = require('crypto'); const auth = getAuth(); const validateSession = async () => { const user = auth.currentUser; if (!user) return; const idTokenResult = await user.getIdTokenResult(); const activeSession = idTokenResult.claims.activeSession; const deviceId = localStorage.getItem('deviceId'); const refreshToken = user.refreshToken; const tokenHash = crypto.createHash('sha256').update(refreshToken).digest('hex'); if (activeSession.deviceId !== deviceId || activeSession.tokenHash !== tokenHash) { await signOut(auth); alert('您的账户已在其他设备登录'); } }; // 在请求受保护资源前调用validateSession validateSession().then(() => { // 发起资源请求 });
内容的提问来源于stack exchange,提问作者Aman Ullah
相关产品推荐
相关产品推荐

