挤兑登录到底是如何实现的?

情景描述

您的帐号于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信息、拦截器判断是否被挤兑...当然,这些代码省略了很多优化的细节,这里就不展开了,要不然又是天花乱坠扯一堆。

此篇到此结束,感谢大家浏览,如有描述不当之处烦请及时指出!如还有不明之处,不妨试着多看几遍.

发布了112 篇原创文章 · 获赞 303 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_36221788/article/details/94564333