情景描述
您的帐号于2019-07-03 09:20:30在另一地点(ip:127.0.0.1)登录,您已被迫下线。这样的情况在生活中也见到很多了吧,最常见的像QQ、微信不同电脑端的登录,前面登录的账号会被挤兑下线。
其实这些互相挤兑登录的情景,都可以总结为同一个账号在不同设备上的互相挤兑,很明显满足挤兑需要两个条件:
1.同一个账号 2.不同设备上登录
第一次接手这个需求的时候,也是有些懵,怎么才算挤兑?如何挤兑? 仔细想想,挤兑登录挤兑登录,首先要登录啊,后台某个用户登录凭证是啥呢?很明显,session中保存的用户信息。那我们的挤兑需求貌似有了大体思路,第一次用户登录后,后台会在session中记录用户登录信息,第二次在另一设备上登录时,判断当前账号是否在其他设备上有session登录信息,有的话把之前的登录信息标记为被挤兑,然后保存新设备的登录信息。
代码实战
先来看看我们的一些涉及的实体,其中get()/set()方法已省略
SessionInfo:记录保存在session中的登录信息
public class SessionInfo {
//用户id
private Long userId;
//用户数手机号
private String phone;
//登录平台:ios/android/web
private String platform;
//会话有效期(秒), 默认31天
protected Integer validSeconds = 2678400;
}
ExtrusionSessionInfo:继承SessionInfo,标识被挤兑的SessionInfo
public class ExtrusionSessionInfo extends SessionInfo {
//挤兑时间
private Date extrusionTime;
//挤兑IP
private String extrusionIp;
}
LoginLogEntity:用来记录登录信息的数据表实体
public class LoginLogEntity extends BaseEntity {
@Id
@GeneratedValue(generator = RedisIdGenerator.NAME)
private Long loginLogId;
//登录用户
@NotNull
@ManyToOne(optional = false, fetch = FetchType.LAZY)
private UserEntity user;
//登录用户ip
@Length(max = 16)
@Column(length = 16)
private String loginIp;
//登录用户sid
@Length(max = 191)
@Column(length = 191)
private String httpHeaderSid;
//登录状态 LOGINED:已登入/EXTRUSION:被挤出/LOGOUT:主动登出
private LoginState state;
//挤兑用户ip
@Length(max = 16)
@Column(length = 16)
private String extrusionIp;
//挤兑时间
private Date extrusionTime;
//登录时间
@CreatedDate
private Date loginTime;
//登出时间
private Date logoutTime;
//登录平台
private String platform;
}
新设备账号登录流程
- 新设备调用登录接口登录
- 查询LoginLogEntity表,是否存在当前用户登录信息,且状态是已登录
state=LOGINED
,存在,说明在其他设备上登录了该账号 - 将之前保存在LoginLogEntity中登录信息的登录状态变更为被挤兑
state=EXTRUSION
,同时根据LoginLogEntity保存的sid清除原session登录信息,清除原session这一步很重要,涉及到getSessionInfo()能否获取出挤兑信息
- 记录当前账号在新设备上的登录信息,同时session中保存登录信息
- 新设备登录完成
以上流程走过之后,我们把原登录信息在数据库中标记出了被挤兑
,剩下就是老设备要提示“您的帐号于2019-07-03 09:20:30在另一地点(ip:127.0.0.1)登录,您已被迫下线”的信息。这个提示可以是后台主动推送,也可以是前端调用接口提示,我们这边的方案是前端调用接口方式提示
,后台主动提示实时性好,处理方案可以对接一些第三方的移动推送,这里就不多说了。
旧设备账号提示被挤兑流程
emm…码了好多字了,先来一段代码解乏:
public class SessionInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String reqIp = RequestContextHolderEx.getRealIpAddress();
SessionInfo sessionInfo = sessionContext.getSessionInfo();
//账号登录挤兑
if (sessionInfo instanceof ExtrusionSessionInfo) {
if (logger.isDebugEnabled()) {
String sid = sessionContext.getSid();
logger.debug("账号登录挤兑 " + reqIp + " " + path + " sid=>" + sid);
}
ExtrusionSessionInfo extrusionSessionInfo = (ExtrusionSessionInfo) sessionInfo;
String msg = String.format(
"您的帐号于%s在另一地点(ip:%s)登录,您已被迫下线",
extrusionSessionInfo.getExtrusionTimeString(),
extrusionSessionInfo.getExtrusionIp()
);
throw new BusinessException("403", msg);
} else if (sessionInfo == null) {
throw new BusinessException("403", "请先登录云实养车");
}
return true;
}
}
啊啊啊,开心,终于看到“您的帐号于%s在另一地点(ip:%s)登录…”这几个字了。这段代码很简单,就是一个拦截器,里面的逻辑就是如果SessionInfo是标识了挤兑即ExtrusionSessionInfo,直接抛出自定义异常提示账号被挤兑,重要的是sessionContext.getSessionInfo()
这个方法返回的东西,咱们瞅瞅去。
public SessionInfo getSessionInfo() {
HttpSession session = request.getSession(false);
if (null == session) {
String sid = getSid();
if (null != sid) {
//sid不为空,session为空,说明账号被挤了
try {
extrusionSessionInfoCache.get(sid, () -> {
Optional<Map<String, Object>> row = jdbcTemplate.queryForList(
"SELECT log.extrusion_time AS extrusionTime,log.extrusion_ip AS ip " +
"FROM sm_login_log log " +
"WHERE log.http_header_sid=? AND log.state=?", sid, LoginState.EXTRUSION.ordinal())
.stream()
.findFirst();
//如果存在被挤兑记录,返回ExtrusionSessionInfo
if (row.isPresent()) {
return Optional.of(new ExtrusionSessionInfo((java.sql.Timestamp) row.get().get("extrusionTime"), (String) row.get().get("ip")));
}
return Optional.empty();
});
} catch (ExecutionException e) {
//ignore
}
}
return null;
}
}
这里首先因为前面新设备登录的时候清除了老设备的session信息
,所以他满足session==null条件,里面的逻辑就是查询登录日志表,根据当前用户sid和登录状态是挤兑状态查询,如果存在的话说明就是被挤兑了,后面直接封装返回ExtrusionSessionInfo,拦截器那边判断出是ExtrusionSessionInfo类型,则直接提示客户端该账号被挤兑了。
总结
整个挤兑流程就这样,其实整个操作下来不复杂,无非就是记录登录信息、记录挤兑信息、清除session信息、拦截器判断是否被挤兑...
当然,这些代码省略了很多优化的细节,这里就不展开了,要不然又是天花乱坠扯一堆。
此篇到此结束,感谢大家浏览,如有描述不当之处烦请及时指出!如还有不明之处,不妨试着多看几遍.