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),账号密码一样,在多个浏览器登录成功,则可以看到效果。
    最后说下:本案的代码时间比较仓促,写的有点粗糙,见谅,有问题私聊,谢谢。

版权声明:本文为keyingbo2008原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。