大家好,我是小悟
1、寫在前面的話
生活中,我們在使用一些APP的時候,有過一種體驗,就是在A手機上登錄賬號,因為某些原因需要在B手機上登錄,然后就會在A手機上看到類似"該賬號在其他設(shè)備登錄"的提示,像下面這樣:

這種方式叫單設(shè)備登錄,作用很明顯,就是為了保護用戶賬號安全,今天我們不說手機APP,我們來說說PC Web網(wǎng)站如何簡單快速實現(xiàn)這種效果。本篇文章重點是實現(xiàn)單設(shè)備登錄,內(nèi)容未涉及WebSocket + Redis的概念和使用方法。限于本人經(jīng)驗,如有錯誤,歡迎指正。
2、概念
簡單的給”單設(shè)備登錄“定義一下,就是只能在一個設(shè)備上登錄,若同時在其他設(shè)備登錄,先前登錄的用戶會被提醒:該賬戶在其他設(shè)備登錄。例如微信,在一臺手機登錄中,同時拿另一臺手機登錄該賬戶,之前那部手機的賬戶會被擠下線。
3、思路
使用此方案的前提是要保證登錄賬號沒有重復(fù)
在socket里面創(chuàng)建兩個Map,sessionPool和sessionIds,分別用來存放客戶端會話池和客戶端會話標(biāo)記
用戶登錄賬號成功,進入應(yīng)用首頁,和服務(wù)端建立socket連接,將賬號+"_"+UUID格式的字符串作為state參數(shù)的值
在OnOpen連接時,以state為key,當(dāng)前session為value存入sessionPool,以sessionId為key,state為value存入sessionIds
以"_"分隔state成數(shù)組,取第一個元素即獲取到當(dāng)前登錄的賬號
在緩存(Redis)里模糊查詢含有該賬號的key集合,如果存在,那么就取出對應(yīng)的value值,其實這個value存的就是首先登陸這個賬號的那個state,就可以根據(jù)這個state給先登錄的賬號設(shè)備推送消息并做logout的操作,并清除緩存
把當(dāng)前登錄的state作為key和value存入緩存,失效時間設(shè)置與否都可以,如果設(shè)置的話需超過登錄態(tài)失效的時長
4、代碼實現(xiàn)
這里貼上幾段核心代碼
后臺WebSocket-On0pen,切記如果設(shè)置緩存失效時間的話需超過登錄態(tài)失效的時長
/**
* 連接時觸發(fā)
* @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()) + "在另一臺設(shè)備登錄,如果這不是您的操作,那么您的登錄密碼已泄露,請盡快修改",value);
redisService.delete(value);
}
}
redisService.set(state,state,7200);
}
/**
* 自定義發(fā)送消息的方法
* @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('當(dāng)前瀏覽器 不支持 websocket')
}
//連接成功建立的回調(diào)方法
websocket.onopen = function () {
console.log('websocket連接成功');
};
//連接發(fā)生錯誤的回調(diào)方法
websocket.onerror = function () {
console.log('websocket連接發(fā)生錯誤');
};
//接收到消息的回調(diào)方法
websocket.onmessage = function (event) {
$.getJSON("logout", function(r){
console.log('logout:'+event.data);
});
layer.confirm(event.data, {
btn: ['確定'] //按鈕
}, function(){
location.href = 'login.html';
});
};
//連接關(guān)閉的回調(diào)方法
websocket.onclose = function () {
console.log("websocket連接關(guān)閉");
};
//監(jiān)聽窗口關(guān)閉事件,當(dāng)窗口關(guān)閉時,主動去關(guān)閉websocket連接,防止連接還沒斷開就關(guān)閉窗口,server端會拋異常。
window.onbeforeunload = function () {
websocket.close();
};
}
5、驗證效果
打開谷歌瀏覽器,賬號密碼登錄
打開火狐瀏覽器,模擬不同設(shè)備,賬號密碼登錄
再看下谷歌瀏覽器,頁面彈窗提示

點擊確定會跳轉(zhuǎn)到登錄頁,谷歌瀏覽器的賬號已經(jīng)被擠下線退出應(yīng)用
谷歌瀏覽器再次登錄,看下火狐瀏覽器,也彈窗提示,之前登錄的賬號也被擠下線退出應(yīng)用

您的一鍵三連,是我更新的最大動力,謝謝
山水有相逢,來日皆可期,謝謝閱讀,我們再會
我手中的金箍棒,上能通天,下能探海