大家好,我是小悟
1、写在前面的话
生活中,我们在使用一些APP的时候,有过一种体验,就是在A手机上登录账号,因为某些原因需要在B手机上登录,然后就会在A手机上看到类似"该账号在其他设备登录"的提示,像下面这样:
这种方式叫单设备登录,作用很明显,就是为了保护用户账号安全,今天我们不说手机APP,我们来说说PC Web网站如何简单快速实现这种效果。本篇文章重点是实现单设备登录,内容未涉及WebSocket + Redis的概念和使用方法。限于本人经验,如有错误,欢迎指正。
2、概念
简单的给”单设备登录“定义一下,就是只能在一个设备上登录,若同时在其他设备登录,先前登录的用户会被提醒:该账户在其他设备登录。例如微信,在一台手机登录中,同时拿另一台手机登录该账户,之前那部手机的账户会被挤下线。
3、思路
使用此方案的前提是要保证登录账号没有重复
在socket里面创建两个Map,sessionPool和sessionIds,分别用来存放客户端会话池和客户端会话标记
用户登录账号成功,进入应用首页,和服务端建立socket连接,将账号+"_"+UUID格式的字符串作为state参数的值
在OnOpen连接时,以state为key,当前session为value存入sessionPool,以sessionId为key,state为value存入sessionIds
以"_"分隔state成数组,取第一个元素即获取到当前登录的账号
在缓存(Redis)里模糊查询含有该账号的key集合,如果存在,那么就取出对应的value值,其实这个value存的就是首先登陆这个账号的那个state,就可以根据这个state给先登录的账号设备推送消息并做logout的操作,并清除缓存
把当前登录的state作为key和value存入缓存,失效时间设置与否都可以,如果设置的话需超过登录态失效的时长
4、代码实现
这里贴上几段核心代码
后台WebSocket-On0pen,切记如果设置缓存失效时间的话需超过登录态失效的时长
/**
* 连接时触发
* @param state
* @param session
*/
@OnOpen
public void onOpen(@PathParam(value = "state")String state,Session session) {
this.session = session;
sessionPool.put(state, session);
sessionIds.put(session.getId(), state);
String[] arry = state.split("_");
Set<String> keys = redisService.keys(arry[0] + "*");
if (keys != null) {
List<String> list = redisService.multiGet(keys);
for (String value : list) {
sendMessage("您的账号于"+ DateUtils.pageformat(DateUtils.getCurrentTime()) + "在另一台设备登录,如果这不是您的操作,那么您的登录密码已泄露,请尽快修改",value);
redisService.delete(value);
}
}
redisService.set(state,state,7200);
}
/**
* 自定义发送消息的方法
* @param message
* @param state
*/
public static void sendMessage(String message,String state) {
Session session = sessionPool.get(state);
if (session != null) {
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
查看缓存
前端js连接WebSocket方法
function getUUID() {
return 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = Math.random() * 16 | 0,
v = (c === 'x' ? r : (r & 0x3 | 0x8));
return v.toString(16);
});
}
function loginSocket(userName) {
var websocket = null;
if ('WebSocket' in window) {
websocket = new WebSocket("ws://localhost:8080/mobile/socketServer/"+userName+"_"+getUUID());
} else {
layer.alert('当前浏览器 不支持 websocket')
}
//连接成功建立的回调方法
websocket.onopen = function () {
console.log('websocket连接成功');
};
//连接发生错误的回调方法
websocket.onerror = function () {
console.log('websocket连接发生错误');
};
//接收到消息的回调方法
websocket.onmessage = function (event) {
$.getJSON("logout", function(r){
console.log('logout:'+event.data);
});
layer.confirm(event.data, {
btn: ['确定'] //按钮
}, function(){
location.href = 'login.html';
});
};
//连接关闭的回调方法
websocket.onclose = function () {
console.log("websocket连接关闭");
};
//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function () {
websocket.close();
};
}
5、验证效果
打开谷歌浏览器,账号密码登录
打开火狐浏览器,模拟不同设备,账号密码登录
再看下谷歌浏览器,页面弹窗提示
点击确定会跳转到登录页,谷歌浏览器的账号已经被挤下线退出应用
谷歌浏览器再次登录,看下火狐浏览器,也弹窗提示,之前登录的账号也被挤下线退出应用
您的一键三连,是我更新的最大动力,谢谢
山水有相逢,来日皆可期,谢谢阅读,我们再会
我手中的金箍棒,上能通天,下能探海