某日小明得知iPlan推出了个人资源库功能,兴致勃勃的申请了账号想尝个鲜。

可是小明是个不爱吃独食的好同学,于是他把自己的账户分享给了好机油小智。花了一份钱实际得到了两个账户,小明暗暗为自己的智商点了个赞。


蜜月期总是充满惊喜的,

刚打开资源库,发现好几个分类已经在那儿摆好造型等我光顾了

直接搜索机器人,能想到的机器人都有了,想用哪个用哪个

菜篮子都填的严严实实的,也不用费脑子选这选那了


然而小明的喜悦才持续几分钟就发现了不对。

随便打开个分类,系统就显示:“对不起,您操作的分类可能已删除”

编辑个资源看看?系统提示:“对不起,该资源已删除”

好嘛,那我加到菜篮子里先,然而过一会儿再去看的时候,菜篮子已经空空如也


什么鬼?这iPlan不会骗钱吧,赶紧找客服,这不找不知道一找吓一跳。客服提示:您的账户在异地登录并进行了大量操作,请保管好你的账号


一切已经真相大白了,还是好机油搞的鬼,看来自己的账户还是不能随便分享给别人啊。
咋办呢?
改密码?这是要友尽的节奏啊;
直接说?丢不了这人啊。


正在小明内心万马奔腾的档口,iPlan客服又很贴心的给出了反馈:
我们鉴于部分用户在多台机器终端使用同一账户的操作可能造成数据错误和体验下滑,大制决定完善账户管理规则,单账户同时只能在一台终端使用。后续发布会加上限制措施,确保用户体验。


这客服也太贴心了,小明立马找到好机油传达了这个反馈,机油也给力,当即决定自己也买个账号以后使用资源分享功能一样可以在iPlan协(gao)作(ji)。
本来到这里,小明就可以坐等升级继续搞基了,然后小明是个闲不住的主,打电话给客服一定要弄清楚限制措施是怎么实现的。

——————————正经的分割线——————————



小明遇到的问题,我们将他称为单一登录问题,区别于单点登录(SSO)

单一登录是指同一账号只允许在一台终端(浏览器,PAD)进行登录和相关操作,主要适用于应用型系统,可以有效的防止数据错误和脏操作。
单点登录则是指对于多系统采用统一的认证机制,以至于用户只需要登录一次就可以获得多个系统的信任进行相关操作,主要适用于企业业务整合解决方案。

如果不实现单一登录会有什么问题呢,

  1. 你看到的东西可能不是最新的(其他终端正在编辑呢)
    小明看到了一个分类,点进去发现小智在其他终端已经删除了这个分类

  2. 你编辑进去的东西可能不会保存(其他终端可能马上就把你改掉了)
    小明加了很多东西在菜篮子里,过一会儿再进去看小智已经帮你清空了

仔细看看这不就是我们数据库事务经常遇到了不可重复读和幻读问题吗?
我们的解决思路也可以参照数据库事务的控制策略,加入版本控制(MVCC)的用户登录级别控制。思路如下:


a) 小明首次登录的时候,服务器针对xiaoming这个账户分配一个版本号的令牌,并在小明本地和服务器上同时保存,如图所示(xiaoming-0001)

b) 之后小明每一次和服务器对话都需要验证一下令牌,
小明:我是小明,此地无银三百两
服务器:一枝红杏出墙来,成交

c) 在小明玩的正high的时候,好
机油小智上线,当然用的还是xiaoming这个账号,于是服务器很公平的给了小智一个新的令牌(xiaoming-0002),
小智也开始不停的和服务器对话了

小智:我是小明,天王盖地虎
服务器:宝宝心里苦,成交

d) 轮到小明了
小明:我是小明,此地无银三百两
服务器:宝宝心里苦,失败。
服务器:您已经成功被绿,请重新登录。

于是乎小明就只能眼睁睁的看着曾今的最爱iPlan和好机油小智眉来眼去了。


那么有没有办法不被绿呢?
很简单,捂好你的账号,再好的机油也不能借。
或者土豪一点直接买个账号送给机油,这不刚好过年了嘛。


——————————技术的分割线——————————

最后在技术实现层面进行简略说明,满足一下小明的好奇心。其实这个功能实现的重点在于令牌的生成,需要确保不同终端用户获取到的令牌是不一致的。

  1.  很容易就想到的一个策略是通过系统时间来作为生成令牌的基础,这当然是一个思路,毕竟毫秒级完全相同的请求概率非常小;

  2.  通过http头信息获取用户登录ip也是一种可行的策略,这种方式需要解决通过代理登录的伪IP或者隐藏IP;

  3. 还有一种直接使用sessionID作为生成令牌的基础,因为sessionID是由服务器生成并保证唯一性的随机值


最终考虑方案复杂度,选定sessionID作为令牌的生成基础。在动手之前先梳理一下session在tomcat下的工作机制。

cookie flow.png

1) 客户端向服务器发出请求
2) 服务器生成session,并返回sessionId给客户端,包含setCookie在头部
3) 浏览器将sessionId保存在cookie中,失效期为会话结束
4) 正常会话的时候在request head携带sessionId
5) 会话结束(关闭浏览器),cookie失效


session lifecycle.jpg


实现步骤

1, 登录方法

if (userKeyOnHandService.findUserToKen(userName)) {
	userKeyOnHandService.replaceUserToken(userName, httpSession.getId());
} else {
	userKeyOnHandService.addUserToken(userName, httpSession.getId());
}


2, 拦截器方法

boolean validateResult = userTokenOnHandService.validateToken(userName, request.getSession().getId());

//validate token using userName and sessionId
public boolean validateToken(String userName, String token) {
	if (this.exists(USER_TOKEN_PREFIX + userName) && this.get(USER_TOKEN_PREFIX + userName).equals(token)) {
		return true;
	}
	return false;
}