You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Firebase中限制用户仅拥有一个活跃认证会话?

实现Firebase用户单设备登录的最优方案

方案一:Admin SDK + 会话时间戳控制(推荐)

核心思路:通过Firebase Admin SDK设置用户的tokensValidAfterTime字段,让所有早于当前登录时间的旧会话刷新令牌失效,仅保留当前登录的会话。

  1. 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);
  1. 云函数中用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,过期后自动退出;当前设备会话不受影响。

方案二:登录事件触发器 + 强制撤销所有旧会话(简单直接)

监听用户登录事件,触发撤销该用户所有会话,当前设备需重新登录(适合对无缝体验要求不高的场景)。

  1. 云函数监听登录事件
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}`);
});
  1. 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记录当前活跃的设备标识和令牌哈希,客户端每次请求时验证会话合法性,适合需要精准控制设备的场景。

  1. 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 });
  });
  1. 云函数设置自定义Claim
exports.setActiveSession = functions.https.onCall(async (data) => {
  const { uid, deviceId, tokenHash } = data;
  await admin.auth().setCustomUserClaims(uid, {
    activeSession: { deviceId, tokenHash }
  });
  return { success: true };
});
  1. 客户端请求前验证会话合法性
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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 01:25:18