如何防止同一用户同时登录 Firebase?

Posted

技术标签:

【中文标题】如何防止同一用户同时登录 Firebase?【英文标题】:How to prevent simultaneous logins of the same user with Firebase? 【发布时间】:2014-02-14 18:31:41 【问题描述】:

我希望新会话基本上“退出”任何以前的会话。例如,当您在一台计算机上进行身份验证会话时,在另一台计算机上启动新会话并在我们的应用上使用 firebase 进行身份验证将注销第一台计算机上的另一个会话。

我找不到任何方法可以让我“远程”退出会话。我知道我可以在会话中进行 unauth() 和 goOffline() 。但是如何从同一用户的不同身份验证会话中执行此操作?

感谢您的帮助!

背景信息:

    我正在使用简单的电子邮件/密码登录进行 Firebase 身份验证 我还没有设置安全规则,虽然这是在工作中 我在 Firebase 中使用 javascript

【问题讨论】:

打开一个新标签仍然算作同一个浏览器会话。如果不将用户从两个选项卡中注销,我认为您无法做到这一点。您可以处理不同的浏览器。 谢谢 jammykam,我可以同时打开两个标签页。但是我要避免使用两种不同的浏览器(在两台不同的计算机上)。 【参考方案1】:

所以我刚刚遇到了这个问题,并且相信我已经以比这里提到的任何方法更容易的方式解决了它,所以我想我会让你知道我最终做了什么。 首先,我使用的是“实时数据库”,所以这个例子使用了它。我有一个“用户”表,每个用户都有一个条目。除其他事项外,这些条目包括“lastLogin”时间戳。当用户登录时,我会更新该表中的字段并输入 lastLogin 时间戳。是的,我知道用户元数据中有一个登录时间戳,但你马上就会明白我为什么这样做。

用户登录后,我只需在该表上设置一个“手表”即可。例如

   var firstTime = true;
   var ref = firebase.database.ref(PATH_TO_TABLE_OR_FIELD);
    ref.on('value',(snapshot)=> 
      if(!firstTime) 
      // CHECK THE SNAPSHOT FOR THE LOGIN FIELD. SINCE IT HAS CHANGED THAT MEANS ANOTHER USER LOGGED IN WITH SAME USERNAME 
       else 
        firstTime = false;
      
    );

“firstTime”值是因为第一次设置“on”时它会立即被调用。因此,我们不想将其视为正在更改的值。之后,只要指定表中的值发生变化,就会调用它。

【讨论】:

【参考方案2】:

TL;DR

https://pastebin.com/jWYu53Up


我们也遇到过这个话题。尽管线程已经过时,而且它并没有完全勾勒出我们想要实现的完全相同的愿望,但我们可以吸收@kato 答案的一些一般概念。概念大致保持不变,但这个线程绝对值得一个更新的答案。

提醒:在您立即阅读此解释之前,请注意您可能会发现它有点脱离上下文,因为它并未完全涵盖原始 SO 问题。事实上,组装一个系统以防止同时出现多个会话是一种不同的心理模型。更准确地说,是我们的心智模型适合我们的场景。 :)

例如,当您在一台计算机上进行经过身份验证的会话时,在另一台计算机上启动新会话并在我们的应用上使用 firebase 进行身份验证将注销第一台计算机上的另一个会话。

维护这种类型的“同时登录预防”意味着 1) 应该区分每个客户端的活动会话,即使它来自同一设备 2) 客户端应该从 AFAICT Firebase 所在的特定设备退出t 能够。 FWIW,您可以revoke tokens 明确地使指定用户的所有刷新令牌都过期,因此,系统会提示您再次登录,但这样做的缺点是它会破坏所有现有会话(即使只是一个已激活)。

这些“开销”导致以稍微不同的方式解决问题。不同之处在于 1) 无需跟踪具体设备 2) 客户端以编程方式退出,而无需破坏其任何活动会话以增强用户体验。


利用Firebase Presence 来完成跟踪客户端连接状态变化的繁重任务(即使连接由于某种奇怪的原因而终止)但这里有一个问题:它本身不是与 Firestore 一起提供。请参阅Connecting to Cloud Firestore 以保持数据库同步。还值得注意的是,与他们的示例相比,我们没有设置对特殊 .info/connected 路径的引用。相反,我们利用onAuthStateChanged() 观察者来响应身份验证状态的变化。

const getUserRef = userId => firebase.database().ref(`/users/$userId`);

firebase.auth().onAuthStateChanged(user => 
   if (user) 
      const userRef = getUserRef(user.uid);

      return userRef
         .onDisconnect()
         .set(
            is_online: false,
            last_seen: firebase.database.ServerValue.TIMESTAMP
         )
         .then(() =>
            // This sets the flag to true once `onDisconnect()` has been attached to the user's ref.
            userRef.set(
               is_online: true,
               last_seen: firebase.database.ServerValue.TIMESTAMP  
            );
         );
   
);

正确设置onDisconnect() 后,如果用户的会话尝试与另一个活动会话一起启动登录,则必须确保用户的会话,为此,将请求转发到数据库并检查相应的标志.因此,由于这种额外的往返,识别多个会话比平时需要更多的时间,因此应该相应地调整 UI。

const ensureUserSession = userId => 
   const userRef = getUserRef(userId);

   return userRef.once("value").then(snapshot => 
      if (!snapshot.exists()) 
         // If the user entry does not exist, create it and return with the promise.
         return userRef.set(
            last_seen: firebase.database.ServerValue.TIMESTAMP
         );
      

      const user = snapshot.data();

      if (user.is_online) 
         // If the user is already signed in, throw a custom error to differentiate from other potential errors.
         throw new SessionAlreadyExists(...);
      

      // Otherwise, return with a resolved promise to permit the sign-in.
      return Promise.resolve();
   );
;

将这两个 sn-ps 组合在一起产生https://pastebin.com/jWYu53Up。

【讨论】:

此解决方案将触发每个用户 onAuthchanged 的​​写入请求。这不是一些沉重的开销吗? 这个解决方案是否完全在客户端实现(所有逻辑)?如果是这样,它安全吗?【参考方案3】:

一般的想法是您想在 Firebase 中创建一些元数据,告诉您用户从多少个位置登录。然后,您可以使用此信息限制他们的访问。

为此,您需要生成自己的令牌(以便您的安全规则可以使用该信息)。

1) 生成令牌

使用custom login 生成您自己的令牌。每个令牌都应包含客户端的唯一 ID(IP 地址?UUID?)

var FirebaseTokenGenerator = require("firebase-token-generator");
var tokenGenerator = new FirebaseTokenGenerator(YOUR_FIREBASE_SECRET);
var token = tokenGenerator.createToken( id: USER_ID, location_id: IP_ADDRESS );

2) 使用存在来存储用户的location_id

查看managing presence 入门了解详情:

var fb = new Firebase(URL);

// after getting auth token back from your server
var parts = deconstructJWT(token);
var ref = fb.child('logged_in_users/'+token.id);

// store the user's location id
ref.set(token.location_id);

// remove location id when user logs out
ref.onDisconnect().remove();

// Helper function to extract claims from a JWT. Does *not* verify the
// validity of the token.
// credits: https://github.com/firebase/angularFire/blob/e8c1d33f34ee5461c0bcd01fc316bcf0649deec6/angularfire.js
function deconstructJWT(token) 
  var segments = token.split(".");
  if (!segments instanceof Array || segments.length !== 3) 
    throw new Error("Invalid JWT");
  
  var claims = segments[1];
  if (window.atob) 
    return JSON.parse(decodeURIComponent(escape(window.atob(claims))));
  
  return token;

3) 添加安全规则

在安全规则中,强制只有当前唯一位置可以读取数据


  "some_restricted_path": 
     ".read": "root.child('logged_in_users/'+auth.id).val() === auth.location_id"
  

4) 控制对logged_in_users 的写入权限

您需要设置一些系统来控制对logged_in_users 的写访问。显然,用户应该只能写入自己的记录。如果您希望第一次登录尝试始终获胜,则使用 ".write": "!data.exists()" 阻止写入(如果存在值)(直到他们注销)

但是,您可以通过允许最后一次登录来大大简化,在这种情况下,它会覆盖旧的位置值,并且以前的登录将失效并且无法读取。

5) 这不是控制并发数的解决方案

您不能使用它来防止 Firebase 发生多个并发事件。请参阅 goOffline() 和 goOnline() 以获取有关完成此操作的更多数据(或获取付费计划,这样您就没有硬性连接上限)。

【讨论】:

感谢您提供非常详细的解决方案。现在,我正在将个人用户的数据直接写入通过他们的转义电子邮件地址索引的 firebase。是否可以将 location_ID 写入通过简单的电子邮件/密码登录生成的 UID 密钥(在他们的电子邮件地址索引表下)并使用该标识符来控制写访问(而不是自定义安全令牌)? 简单登录无法区分来自不同位置的用户。由于令牌中没有任何内容,因此您需要创建自己的令牌以使其可用于安全规则。 客户会立即识别还是仅在发出其他请求时才识别? 这篇文章来自 2014 年。不确定这是否适用。 @Kato - 我是否理解这个答案需要在后端运行节点服务器?我纯粹在前端使用firebase。这样做安全吗?

以上是关于如何防止同一用户同时登录 Firebase?的主要内容,如果未能解决你的问题,请参考以下文章

Firebase:防止在多个设备上使用相同的帐户

如何防止 Firebase 中出现重复的用户属性?

防止用户在注册firebase后自动登录[重复]

如何限制firebase中的登录

如何使用 Firebase 身份验证服务并在我们自己的数据库中拥有用户?

防止使用相同的登录凭据进行多次登录