Websocket案例二:賬號多端登錄踢出

前言

上一篇介紹了websocket的原理,以及一個聊天室的Demo上一篇,本文繼續基於websocket實現一個賬號多端登錄踢出的案例。主要的流程程如圖:
實現大體的登錄流程
主要流程說明:

  1. 瀏覽器客戶端1用賬號001登錄成功後,服務端會生成token,並記錄在服務端緩存。
  2. 服務端的token返回給客戶端,客戶端保存於本地cookie中。然後,基於token的方式與服務端建立websocket連接。注意這裏瀏覽器可能會刷新,會重新關閉連接再重連。
  3. 客戶端2用賬號001登錄成功後,服務端會重新生成token,會重新記錄緩存。
  4. 這裏跟2的動作一樣。
  5. 服務端會通知其他端進行退出操作(這裏需要注意的是:同一個瀏覽器可能打開多個標籤,這時所有標籤的登錄頁都不做退出處理,這裏就涉及同一個用戶會連接多個websocket的現象)

技術棧

具體參考上一篇,用的是一樣的技術,且在同一個源碼下。

主要代碼說明

LoginController

@Controller
public class LoginController {
    ScheduledExecutorService service = Executors.newScheduledThreadPool(4);

    @Autowired
    private WebSocketKeepOneLoginServer webSocketKeepOneLoginServer;

    private static Map<String, UserDto> userDtoMap = new HashMap<>();
    static {
        //用戶集合
        userDtoMap.put("user001" ,UserDto.builder().userId("user001").psw("user001").userName("用戶1").build());
        userDtoMap.put("user002" ,UserDto.builder().userId("user002").psw("user002").userName("用戶2").build());
        userDtoMap.put("user003" ,UserDto.builder().userId("user003").psw("user003").userName("用戶3").build());
    }

    @RequestMapping("login")
    public String login(HttpServletRequest request,LoginDto loginDto) {
        String result = "loginerror";
        if (StringUtils.isEmpty(loginDto.getUserId())) {
            request.setAttribute("error", "用戶ID不能爲空");
            return result;
        }
        if (!userDtoMap.containsKey(loginDto.getUserId())) {
            request.setAttribute("error", "用戶ID或密碼不正常");
            return result;
        }
        UserDto userDto = userDtoMap.get(loginDto.getUserId());
        if (!userDto.getPsw().equals(loginDto.getPsw())) {
            request.setAttribute("error", "用戶ID或密碼不正常");
            return result;
        }
        //登錄成功
        TokenUtil.resetUserAndToken(userDto.getUserId());
        //返回token
        request.setAttribute("token" , TokenUtil.getToken(userDto.getUserId()));
        //通知踢除其他端登錄
        //延時3秒執行
        service.schedule(() ->{
            webSocketKeepOneLoginServer.noticeToOtherWeb(userDto.getUserId());
        }, 3 , TimeUnit.SECONDS);

        result = "loginsucess";
        return result;
    }

    @RequestMapping("index")
    public String index(HttpServletRequest request, @RequestParam("token") String token) {
        request.setAttribute("token" , token);
        return "index";
    }
}

這裏主要有兩個方法:

  1. 登錄
    驗證賬號密碼,成功後設置token,並返回token給客戶端。然後進行通知其他端退出(如果有)
  2. 首頁
    通過token加載首頁

WebSocketKeepOneLoginServer

 //增加socket,一個用戶也可能連接了多端
    private void addWebSocket(String userId , WebSocketKeepOneLoginServer webSocketKeepOneLoginServer) {
        synchronized (WebSocketKeepOneLoginServer.class) {
            List<WebSocketKeepOneLoginServer> webSocketKeepOneLoginServerList = webSocketMap.get(userId);
            if (CollectionUtils.isEmpty(webSocketKeepOneLoginServerList)) {
                webSocketKeepOneLoginServerList = new ArrayList<>();
                webSocketKeepOneLoginServerList.add(webSocketKeepOneLoginServer);
                webSocketMap.put(userId , webSocketKeepOneLoginServerList);
                OnlineCount.incrementAndGet(); // 在線數加1
            } else {
                webSocketKeepOneLoginServerList.add(webSocketKeepOneLoginServer);
            }
        }
    }

這裏是每次客戶端socket連接後,會調此方法,如果判斷是同個用戶多次連接的情況,只是單純增加連接,如果是多用戶則增加在線人數。

 /**
     * 移出一個socket
     * @param userId
     * @param webSocketsession
     */
    private void removeOneSocket(String userId, Session webSocketsession) {
        synchronized (WebSocketKeepOneLoginServer.class) {
            List<WebSocketKeepOneLoginServer> webSocketKeepOneLoginServerList = webSocketMap.get(userId);
            if (!CollectionUtils.isEmpty(webSocketKeepOneLoginServerList)) {
                webSocketKeepOneLoginServerList.remove(webSocketsession);
            }
            //處理完後如果列表爲空,則清除登錄信息
            if (!CollectionUtils.isEmpty(webSocketKeepOneLoginServerList)) {
                webSocketMap.remove(userId);
                int cnt = OnlineCount.decrementAndGet();
                logger.info("有連接關閉,當前連接數爲:{}", cnt);
            }
        }
        webSocketMap.remove(userId , WebSocketsession);//從set中刪除
    }

這裏跟剛纔相反,是客戶端關閉連接後調用邏輯,如果用戶的連接數減少到0,則同時將在線用戶數減一操作。

 /**
     * 通知其他已登錄端退出(如果有)
     * @param userId
     * @throws IOException
     */
    public void noticeToOtherWeb(String userId) {
        List<WebSocketKeepOneLoginServer> webSocketServerList = webSocketMap.get(userId);
        if (webSocketServerList == null) return;

        for (WebSocketKeepOneLoginServer webSocketServer : webSocketServerList) {
            if ( webSocketServer != null && webSocketServer.WebSocketsession.isOpen()){
                sendToken(webSocketServer.WebSocketsession,TokenUtil.getToken(userId));
            }
        }
    }

這個方法就是登錄成功後,調用的通知其他端退出的方法。

loginsucess.html

客戶端比較簡單,這裏主要介紹兩個文件,一個是現在這個,主要的邏輯如下:

<script th:inline="javascript">
    //設置token cookie
    var token = [[${token}]];
    $.cookie("token",token)
    console.log("login sucess:token:" + token)
    window.location = '/index?token=' +  token;
</script>

根據服務端返回的token,然後跳轉到index.html

index.html

var token = [[${token}]];
        $.cookie('token',token);
        var wsurl = 'ws://127.0.0.1:8080/websocketKeepOneLogin/' + token;

這裏主要是根據服務端返回的token值設到cookie裏,並根據當前的token進行socket連接

 //收到消息
 websocket.onmessage = function (event) {
     var token = event.data;//收到token信息
     //不相等,則將此頁面跳轉登錄頁
     if ($.cookie('token') != token) {
         window.location = '/html/login.html';
     }
 }

收到服務端的socket通知,獲取當前用戶真正的token,如果與本地的cookie值不匹配,則說明此用戶已經在其他端登錄了,則直接跳轉到登錄頁面。

後語

以上案例,我本地已經跑通,具體的源碼地址爲:
源碼地址
啓動後,本案例訪問步驟說明:

  1. 打開多個瀏覽器或者多個頁籤輸入:http://localhost:8080/html/login.html
  2. 後臺設置了三個賬號(user001,user002,user003),賬號密碼一樣,在多個瀏覽器登錄成功,則可以看到效果。
    最後說下:本案的代碼時間比較倉促,寫的有點粗糙,見諒,有問題私聊,謝謝。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章