先介绍一下项目背景:公司生产了一些电视机和照相机,同时公开了一些接口,允许开发者基于此开发一些第三方应用。有些开发者可能没有电视机和照相机真机,或者团队中真机不足,为了能够是他们顺利地进行开发,所以我们开发一款软件,让开发者可以申请虚拟设备。该软件中实现了 SSDP 协议让第三方应用发现虚拟设备。
前些天,Tony 作为用户又作为项目的负责人提出一个要求:不能让一个账号同时在两个电脑(或者浏览器)上登录。这下可难倒了我们外包团队。他们的开发人员 W 找到我,说这个该怎么做呢?W 也提出一种方案,就是使用 WebSocket,对于不支持 WebSocket 的浏览器使用轮询来作为 fallback 方案。这个方案可以使用现有的 socket.io 库来轻松实现。
首先讲一下,不让一个账号同时两处登录,这样做的站点一般有聊天室等,有了这样的限制以后,一个用户在某个浏览器上登录以后,再不退出的情况下直接关闭浏览器,用户就不能在其他浏览器上登录了,直到 Session 过期。Session 自动过期时间可长可短,一般是半个小时。用户体验非常不好,更好的策略是,后登录的把先登录的踢掉。
以前我在 J2EE 企业级应用框架时实现了这样的功能,虽然实现了,但是还没有在任何一个项目中实际使用了。实现了,更多是为了功能的完备性。还有一点,在一个实现了完整的认证授权系统中,实现这样的功能是非常简单的。实现的方法如下:
首次成功登录以后,会再数据库的 Session 表中添加一条记录。字段一般包括 Session ID,用户 ID,Cookie,登录时间,最后操作时间(初始化为登录时间)。
以后用户没操作一次,都会修改 Session 表中的最后登录时间。这个在 J2EE 中一般通过 filter 实现。并且服务器端会定期执行一个任务,清除 Session 表中过期 Session。一个 Session 何时过期,一般判断条件是当前距离最后操作时间大于半个小时(可配置)。
当用户再次登录时,首先判断用户名和密码是否正确,如果正确,然后用户是否已经在别处登录,即 Session 表是否有该用户的未过期 Session,如果是,则给用户一个错误提示,否则才判定位登录成功。
在我们当前的应用中,服务器端语言使用的 Node,框架是 Express。Session 管理我们可以使用的是 Express 自带的 Session 中间件。代码如下:
1 | var express = require('express'); |
Express 的 Session 中间件没有 API 可以得到用户是否已经在别处登录。另外一个问题就是即便我们自己实现 Session 控制,那么也是不可取的,因为项目特殊性,用户在登录以后就加在几乎所有的资源,也只有在登录,申请设备和获取已申请设备清单时才会发送一个服务器端的请求。之后用户只需要按下启动按钮,之后虚拟设备就运行在 Web App 中了,第三方应用就可以通过 SSDP 协议发现设备并与该设备进行 HTTP 通信了。以后只有部分静态资源文件会从 Node Server 上获取之后,就没有其他的请求了。所以自从获取已申请设备清单以后,就没有会修改最后操作时间的请求了。
W 给出的一个方案:使用 socket.io,保持与服务器端通信。可行倒是可行,但是我们要为了一个需求大动干戈吗?这个要划一个问号。然后我跟 W 说,我去跟 Tony 商量一下,看看他的目的是什么,再决定怎么做。
在跟 Tony 的交流中,发现他有过这样的经历:开发阶段两个人使用了同一个账号同时登录,这两个浏览器中自然有着同样的设备列表,然后都启动了某个设备,这时候在局域网中就有了两个有着同样 UUID 的虚拟设备,他们 IP 不同,设别描述文件地址也不同,设备状态也不同。有些 SSDP 客户端会因此而崩溃掉。所以他想让一个账号不能同时在两处登录就能解决该问题了。
我告诉 Tony,既然你是为了解决这样的问题,那么我们能否换个思路,让每个浏览器上的设备 UUID 都不相同,在生成 UUID 时,不仅加入设备 ID,还加入浏览器所在电脑的 IP,时间戳,随机值等。这样一来,团队成员可以共享一个账号,在同一个局域网中也可以独立使用,互不影响。然后 Tony 认同了这一做法。到此,这个需求就这么解决了,仅仅修改一下 UUID 的生成算法。
在上面的案例中,可以发现,用户提出的要求可能只是他们所关切的问题的一个表象,或者是他们认为的这个问题的一个解决方案。我们不能仅仅局限于此,而要透过他们的要求去探索他们真正关切的问题。