如何管理Session(防止恶意共享账号)——理论篇

目录

  • 知识要求
  • 背景
  • 技术原理
  • 如何管理Session
  • remember me的问题
  • TODO
  • 附录

知识要求

  • 有一定的WEB后端开发基础,熟悉Session的用法,以及与RedisDatabase的配合
  • 本文的原理讨论基于PHPLaravel,尽管原理是通用的,但是读者具备相关知识理解会更轻松

背景

公司在业务层面上,通常会期望自己运营的系统,一个注册帐号只能由本人使用或者少数几个人共用。实际情况却是:有些用户会将注册帐号的用户名和密码共享给成百上千个用户;也有的用户不直接提供用户名和密码,而是提供带有认证信息的cookie给其他用户,同样达到共享账号的目的。不论哪种形式,都造成了公司业务的损失。因此系统应该具备查看在线用户的功能,并可对在线用户实时管理,防止注册帐号被许多人共享。

技术原理

在技术层面上,在线用户都是用Session表示。

用户通过用户名和密码正常登陆时,就会产生新的Session,退出则对应Session被清除。当多个用户使用同一账号登陆时,就会产生多个Session,防止共享账号数量太多就是要限制同一账号的Session数量。但是,这远远不够。

我们都知道,Session的原理是通过将session id写入cookie,下次浏览器访问时会把cookie带上来,识别其中的session id实现的(laravel存储session_idcookie名称为laravel_session)。如果将该session id传递给其他用户,其他用户再将session id写入cookie,就可以达到共享账号,同时Session仍然是同一个的目的(见下图流程)。这种情况使得在线用户管理变得棘手,好在它们的IP并不相同,所以还是有办法处理。

通过cookie共享Session原理

上面两种形式是共享账号的两种主要形式,于是我们的问题就变成:

  1. 如何管理一个账号对应多个Session的问题
  2. 如何管理一个Session多个IP的问题

下面先对如何管理Session做综述,再对两种情况分开说明。

如何管理Session

管理Session的前提是

  • 系统能够获取到所有的Session
  • 获取Session的所有IP信息。

一个高性能的系统,Session保存在缓存系统,比如Redis中。通过统一约定以session.开头的键为Session,就可以获取到所有Session,但这实际上是个很差的方法。Redis的设计并不是为了实现这样的目的,所以它的键值匹配要么效率极低,要么不能保证返回所有结果,同时它的扩展性非常地差,比如只读取某个用户的所有Session,需要对键的命名再做进一步约束。

于是我们换了另外一种方案,Session仍然保存在缓存系统中,同时异步保存在数据库中,注意必须是异步,否则会影响系统的运行。具体原理是,在Session的写入、销毁、回收这几个阶段发出event,将session连同HTTP请求放到队列中(队列是上下文无关的,获取不到任何HTTP请求的信息,需要从事件中读取),然后队列取出这些事件,写入到数据库。这样就能做到不影响性能,又可获取到所有的Session信息,并做灵活地管理。

Session默认没有携带IP信息,因此在每次Session写入时,需要再做一层加工,将IP写入Session,并且不能只保存一个IP,需要保存多个,以便后续问题的处理。

1.如何管理一个账号对应多个Session的问题

既然数据库中已经保存了所有Session,在有新的Session产生时,检查是否超出指定数量。当超出时,自动删除最早的Session即可。

如何手动测试

设置好要限制的数量,假设为2。安装SessionBoxChrome插件),创建3个窗口以相同用户登陆,将发现最早登陆的窗口刷新后处于未登陆状态。

合理的Session数量

一个用户可能从多个设备登陆,比如PC、手机、平板,所以Session数量至少在3个以上。用户也可能在PC上开N个不同浏览器,导致同一个设备有多个Session,应该优化此种情况,判定为同一个设备。具体看《TODO》这一节说明。

2.如何管理一个Session多个IP的问题

所有的用户共享同一个Session,也就没办法精确控制要保留的数量了。要么删除Session,所有用户重新登陆;要么重新生成session id,只保留一个用户,其他所有用户需要重新登陆。目前采用后者,因为前者有一个风险,同一个用户可能从多个地方登陆产生了大量的IP,结果就踢出去了,用户体验不好。而如果是后者,如果一个用户,只是自己使用,那么不论他的ip数量是否突破限制,重新生成session id仍然是他的,所以不会受影响。

它的原理是用户发起请求,发现IP过多,就重新生成session id,这个新的session id会写到该用户的cookie中,而其他用户由于没有这个新的session id,所以需要重新登陆。


从这个流程中也可看出,这个处理过程必须是在用户发起请求时处理。需要注意的是,这个过程会需要考虑并发,即便是单个用户访问。假设一个用户访问页面,该页面同时发起4个请求。服务端同时处理这4个请求,都发现sessionip过多,于是删除旧session重新生成,造成一个问题:4个请求删除同一个旧session,然后生成了4个不同的session。为防止这种情况,使用了Redis对该session id加锁,并设置30秒自动过期,只有第一个获取锁的人执行重新生成,其他没获得锁的请求不处理。然后锁不用释放,自然过期即可。

正常而言,session已经被重新生成了,旧session id是走不到加锁session id这一步的。如果有,那一定是在重新生成之前就进来的请求,而这些请求本来就应该被忽略。反之,如果删除锁,这些请求将再次加锁并重新生成session,仍然会造成刚才说的问题,因此直接让锁自动过期即可。

如何手动测试

假设IP数量限制为1个,打开A``B两台电脑,在A电脑上先登陆,打开Chrome开发者工具,复制laraval_session的值;然后传到B电脑,打开Chrome开发者工具,设置laravel_session的值,然后刷新下将发现变为登陆状态。再刷新下A电脑,将发现处于未登陆状态。

合理的ip限制数量

这个值则很主观,同时多个用户共享一个Session的问题实际是可避免的,具体参考《Remember Me的问题》的说明。因此不建议设置得太小,建议在5以上。

remember me的问题

如果一个账号在线的存活期只有几个小时,那么上面说的问题影响范围都有限。为提高用户体验,用户一次登陆后可存活好几天甚至永久存活。提高存活时间有两种方法:

  1. 修改Session的过期时间
  2. 使用Remember Me的机制

上面管理Session的方案,在第1种方法下能顺利工作,但是在第2种方法下则没法工作。所以使用了Remember Me的方案,在管理机制上需要重新设计。

要理解这个问题所在,我们需要理解Remember Me的机制:用户登陆后,如果设置了Remember Me,服务器会生成remember me token,保存在数据库中,并将该token写入到cookie中。用户Session过期后,再次访问浏览器,服务端发现cookie中的remember me信息与数据库中的一致,就重新生成新的Session(见流程图)。

通过Remember永久登陆原理

在了解上述机制后,即可发现,当remember me的用户超过session限制数量后,最早的session被删除,但由于该用户有remember me,所以会重新生成session自动恢复,也就说,删除sessionremember me用户无效,会立刻重新生成。所以上述的session管理方案不应该开启remember me,否则是有问题的。

那为什么不直接Remember Me的方案呢?主要原因在于它的设计比较复杂,最终我们会切换成Remember Me的机制,将会另开一篇专门讨论。

TODO

  • [ ] 多个Session可能是同一个设备发出的,因此应该结合IP判断是否是同一个设备,或者结合客户端发出的唯一标识(唯一标识需要是PC的唯一标识)。如果是同一个设备的就都放过的话,其实也有问题,SessionBox这样的工具可以在单个浏览器创建多个Session,如果这个过程脚本化,服务器会产生大量垃圾Session,所以也应该限制数量。
  • [ ] 改为Remember me方案,将问题简化为只考虑一个帐户多个Session的问题。
  • [ ] 客户端发送唯一标识的方式是否具备可行性?

附录

知识点

Q:Session与登陆用户的关系?

A:严格来说,只要用户打开浏览器访问网站,就会产生Session标识一个会话,跟是否登陆无关。但是一般情况下,我们只关心登陆用户的Session,因此这里讨论上不做区分,只要产生Session就认为有登陆用户。

参考

posted @ 2017-11-24 12:40 枫叶落一地 阅读(...) 评论(...) 编辑 收藏