在项目中需要利用userId和时间戳生成订单,这就要保证一个用户在同一时间只能在一个客户端上进行操作,而且为了账号的安全性。所以就想实现QQ那样的后者挤掉前者的功能。
由于自己是一边学一边写的项目,所以对session这些功能的特性没有充分的了解。刚开始以为很简单,直接后者登录时直接查询当前用户信息是否已经存在在session中,如果存在移除掉前者的session信息就可以,因为每个session都有自己的sessionId。这简直不要太简单。结果无知就是天真。首先思路没啥问题,等到去写方法得时候太发现session是每个会话独有的,其他的连接是不能访问其他连接的session的,而且也没有直接通过sessionId移除session这种方法。
于是百度看看别人都是怎么做的。也是利用的session。思路是把session和用户状态存储到一个临时表中,然后给了两种方案,一种是hashMap,一种是servletcontext。两种都不是很了解。于是百度,一下是关于两种的解释:
二者比较之后我觉得servletcontext简单易操作,而且整个服务器端可以共享数据,服务器启动自动生成,关闭即销毁。刚好符合需求。开搞。先上最终效果:
可以看到左边浏览器已经登录了迪丽热巴账号,右边再去登录,这时左边不管是页面刷新还是跳转其他页面,都会重定向到登陆页,并提示账号在其他地方登录!
上代码;
登录成功时,将用户信息存入session中,取出sessionId和用户Id,以用户id为key(因为每个用户id都是唯一的,sessionId会改变的)。
UserInfo userInfo = userRepositories.findByAccountAndPassword(account,password);
session.setAttribute("loginUser",userInfo);/*登录成功把用户信息存入session中,便于调用*/
String userId = String.valueOf(userInfo.getId());/*获取当前用户id并初始化为String类型*/
String sessionId = session.getId();/*获取当前会话的sessionId*/
ServletContext servletContext = GetServletContext.getServletContext();
servletContext.setAttribute(userId,sessionId);/*以用户id为key,sessionId为value存储到容器中*/
session.removeAttribute("loginState");
return "登录成功";
然后写一个检查session的方法,判断当前session中是否有用户登录信息,没有的话说明没登录,返回true,如果存在登录信息,取出当前sessionId以及servletcontext中此用户id对应的sessionId,判断两个sessionId是否相等,相等的话,说明是同一个连接会话,也返回true。不相等的话,就说明后面登录的sessionId已经覆盖掉了之前servletcontext中的sessionId,存在异地登录,返回false。
public class CheckSession{
public static boolean checkSession(){
HttpSession session = GetSession.getSession();
UserInfo userInfo = (UserInfo) session.getAttribute("loginUser");
if (userInfo == null){
return true;
}else{
ServletContext servletContext = GetServletContext.getServletContext();
String sessionId = session.getId();
String userId = String.valueOf(userInfo.getId());
String oldSessionId = (String) servletContext.getAttribute(userId);
if (oldSessionId == null || oldSessionId.equals(sessionId)){/*如果不存在此用户的sessionId(一般不可能)
或者新旧id相等,说明是同一个登录*/
return true;
}else {
return false;/*否则就是不同客户端登录,返回false*/
}
}
}
}
然后写一个移除session的方法,因为我这里每个页面拦截判断后都需要执行。
public class DelSecondUser {
public static void delSecondUser(){
HttpSession session = GetRequest.getRequest().getSession();
session.removeAttribute("loginUser");
session.setAttribute("loginState","1");/*存储一个状态,用于前端判断是多客户端登录,账号被挤掉了*/
}
}
关于在普通类方法中怎么获得session等,可以查看我上一篇博客:springboot普通类获取session等
然后去拦截器那里进行判断:
@GetMapping({"/index", ""})
public String index(ModelMap modelMap) {
if (!CheckSession.checkSession()) {
DelSecondUser.delSecondUser();
return "redirect:user";
} else {
return "index";
}
}
这里我是用@GetMapping来拦截路径请求的,首先就执行checksession判断,如果返回的是false,说明账号已经在其他地方登录了,那么这个就得会话的session就得被移除,所以执行delSecondUser方法,并重定向到登陆页。不是的话就进行正常逻辑操作。每个页面都进行这个逻辑判断。就可以实现只要刷新或跳转页面就会退出。
至于怎么在登录页判断是不是被挤掉账号而过来的进行提示
记得我们在移除session的那个方法里写了这样一个语句。
session.setAttribute("loginState","1");/*存储一个状态,用于前端判断是多客户端登录,账号被挤掉了*/
如果是被挤到的,那么给前端一个1的状态。
然后在登录页放一个隐藏的标签来接收它:
<span style="display:none;"th:if="${session.loginState!=null}"th:text="${session.loginState}" id="ifLogin"></span><!--如果是1代表是被挤掉而重定向过来的-->
<span style="display: none;" th:if="${session.loginState==null}" th:text="0" id="ifLogin"></span><!--默认是0-->
在登录页的js里写一个判断方法:
//判断是不是被挤出登录
function ifLogin(){
var state = $('#ifLogin').text().trim();
if (state == "1"){
layer.confirm('您的账号在其他地方登录,请确认是否本人操作?', {
btn: ['是我本人操作','不是我,立即修改密码'] //按钮
,closeBtn:0
,title:"警告"
,btnAlign: 'c'
}, function(){
layer.msg('好的!请注意保护个人账号安全!',{icon:6});
}, function(){
location.href="forgetPd";
});
}
}
每次进入页面都进行判断,是不是1,是1就给出提示。
如果在登录界面再次登录,那么就移除掉这loginstate状态,前面登录那里有写到。
但是有一种情况,比如涉及到用户登录的情况下才能执行的业务逻辑,假如a客户端用户1正在充值,订单号是需要从session中取出用户id加时间戳生成的,这时候b客户端突然有人用用户1的账号登录了,a客户端的session被移除了,但是a客户端页面没刷新,用户1可以继续点击充值按钮,这时候后台取不到session信息,报错。
所以我们在后台需要执行一个判断,如果取不到session,直接返回一个结果前端判断即可,如果取得到则继续业务操作就行。
至此此功能完整实现。
由于本人比较小白,所以表述上有啥错误或者有更好的方法,欢迎大佬们指正和指教*^_^*