React和SpringBoot前后端分离开发模式下采用WebSocket实现单设备登录

首先介绍下,什么是单设备登录?
单设备登录可以理解为同一个应用某一时刻只允许单一用户使用处于登录状态。单设备登录可以类比QQ的踢出第二者登录模式,可以在一定的程度上保障账号的安全。


单设备登录的难点在哪?
单设备登录的难点在于如何主动、准确的推动消息到客户端。但是幸运的是我们可以通过WebSocket实现消息的准确推送,并且React中也有对应的WebSocket的Js依赖包可以使用。

下面将介绍详细的实现流程:
A、前端开发准备流程:
1、使用npm 安装正确的js依赖包:

npm install websockt

参考文章:阮一峰的WebSocket 教程

2、websocket前端实现代码:

   /**
    * 建立WS链接相关的方法;jSESSIONID:''
    * @param {*} wsUrl :建立webSocket链接的Url;
    */
    estabConnectWithWS(wsUrl) {
    const ws = new WebSocket(wsUrl);
    
    let result = "";

    ws.onopen = function (e) {
      console.log('连接上 ws 服务端了');
      ws.send(JSON.stringify({ flag: wsUrl, data: "Hello WebSocket!" }));
    }
    ws.onmessage = (msg)=> { 
        console.log('接收服务端发过来的消息: %o', msg); 
        var msgJson = JSON.parse(msg.data);
        result += msgJson.MsgBody + '\n';
        if (msgJson.MsgCode == "999999") {//多设备在线的异常发生时;
           window.location.href = '/#/';
        } else if (msgJson.MsgCode == "555555") {//用户退出系统的时候;
            ws.close();
            window.location.href = '/#/';
        }
        alert(msgJson.MsgBody);
    }; 
    ws.onclose = function (e) {
        console.log('ws 连接关闭了');
        console.log(e);
    }
    
  }

主要步骤就是在登录校验通过后就建立和WebSocket服务终端的链接,并启动消息监听,在多用户登录的时候,前端弹出提示消息并跳转到登录页面,在用户执行退出操作的时候终断websocket链接并跳转到登录页面。

具体情境如下图所示:
这里写图片描述
B、websocket后端开发流程
1、在用户成功登录后创建session并存储用户的登录信息;
2、websocket客户端和websocket服务端握手成功之后,将第一步中的session中的数据放入WebSocketSession中,便于在MyWebSocketHandler中创建在线用户表,最终将根据在线用户表中的userId实现一对一的消息推送。
3、ws请求进入MyWebSocketHandler【ws处理】中,在ws链接建立的生命周期中进行合理的数据处理。

WebSocketInterceptor.java

package com.sds.socketInterceptor;

import java.io.IOException;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.server.HandshakeInterceptor;

import com.sds.enums.MsgEnum;
import com.sds.model.LoginerInfo;
import com.sds.model.WSMsg;
import com.sds.socketHandle.MyWebSocketHandler;

public class WebSocketInterceptor implements HandshakeInterceptor {
	@Override
	public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
			Map<String, Object> map) throws Exception {
		if (request instanceof ServletServerHttpRequest) {
			HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest();
			HttpSession session = servletRequest.getSession(false);
			if (session != null) {
				// 处理已经建立的链接;
				dealWithConnectioned(session);
				// 添加自定义的属性值到wobsocket中,便于进行访问控制,注意逻辑上的先后顺序;
				map.put("userInfo", session.getAttribute("userInfo"));
			}

		}
		return true;
	}

	@Override
	public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
			Exception exception) {

	}

	/**
	 * 处理多设备登录;
	 */
	public static boolean dealWithConnectioned(HttpSession userInfoStore) {

		boolean isConnectioned = false;

		LoginerInfo currentUserInfo = ((LoginerInfo) userInfoStore.getAttribute("userInfo"));
		String userId = null;
		if (null != currentUserInfo) {
			userId = ((LoginerInfo) userInfoStore.getAttribute("userInfo")).getUserId();
		} else {
			System.out.println("--CurrentUserInfo--Is----Null--");
		}

		Map<String, WebSocketSession> users = MyWebSocketHandler.users;
		if (null != users) {
			if (users.containsKey(userId)) {
				// 如果已经登录过,则推送"已登录过的提示"到客户端;
				// 客户端接受信息,请求退出系统的接口;
				// 清除session中保存的用户信息;
				WebSocketSession webSocketSession = users.get(userId);
				WSMsg wsMsg = new WSMsg(MsgEnum.FORCE_LOFOUT_MSG.getMsgCode(), MsgEnum.FORCE_LOFOUT_MSG.getMsgBody());
				TextMessage promptMsg = new TextMessage(wsMsg.getMsgJson());
				try {
					webSocketSession.sendMessage(promptMsg);
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				// 从已登录用户列表中清除已经登录过的用户的信息记录;
				users.remove(userId);
				isConnectioned = true;
			}
		}
		return isConnectioned;
	}
}

MyWebSocketHandler.java

package com.sds.socketHandle;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import com.sds.enums.MsgEnum;
import com.sds.model.LoginerInfo;
import com.sds.model.WSMsg;

@Component
public class MyWebSocketHandler extends TextWebSocketHandler {
	// 在线用户列表
	public static final Map<String, WebSocketSession> users = new HashMap<String, WebSocketSession>();

	/**
	 * 连接已关闭,移除在Map集合中的记录
	 */
	@Override
	public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
		users.remove(getUserId(session));
	}

	@Override
	public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
		if (session.isOpen()) {
			session.close();
		}
		System.out.println("连接出错");
		users.remove(getUserId(session));
	}

	/**
	 * 连接建立成功之后,记录用户的连接标识,便于后面发信息
	 */
	@Override
	public void afterConnectionEstablished(WebSocketSession session) throws Exception {
		System.out.println("成功建立连接");
		String userId = getUserId(session);
		System.out.println(userId);
		if (userId != null) {
			users.put(userId, session);
			WSMsg wsMsg = new WSMsg(MsgEnum.CONNECTION_SUCCESS_MSG.getMsgCode(),
					MsgEnum.CONNECTION_SUCCESS_MSG.getMsgBody());
			TextMessage promptMsg = new TextMessage(wsMsg.getMsgJson());
			session.sendMessage(promptMsg);
		}
	}

	/**
	 * 处理收到的websocket信息
	 */
	@Override
	protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {

	}

	/**
	 * 发送信息给指定用户
	 * 
	 * @param clientId
	 * @param message
	 * @return
	 */
	public boolean sendMessageToUser(String userId, String message) {
		if (users.get(userId) == null) {
			return false;
		}

		WebSocketSession session = users.get(userId);
		System.out.println("sendMessage:" + session);

		if (!session.isOpen()) {
			return false;
		}
		try {
			int count = 1;
			TextMessage textMessage = null;
			@SuppressWarnings("unused")
			String newMessage = "";

			// 循环向客户端发送数据
			// while (true) {
			newMessage = message + String.valueOf(count);
			textMessage = new TextMessage(message);
			session.sendMessage(textMessage);
			Thread.sleep(500);
			newMessage = "";
			// }
			return true;
		} catch (IOException e) {
			e.printStackTrace();
			return false;
		} catch (InterruptedException e) {
			e.printStackTrace();
			return false;
		}
	}

	/**
	 * 发送信息给指定用户的浏览器;
	 * 
	 * @param clientId
	 * @param message
	 * @return
	 */
	public boolean sendMessageToTargetFront(String userId, TextMessage message) {
		boolean isSendSuccessfully = true;
		WebSocketSession webSocketSession = users.get(userId);
		try {
			webSocketSession.sendMessage(message);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			isSendSuccessfully = false;
			e.printStackTrace();
		}
		return isSendSuccessfully;
	}

	/**
	 * 广播信息
	 * 
	 * @param message
	 * @return
	 */
	public boolean sendMessageToAllUsers(TextMessage message) {
		boolean allSendSuccess = true;
		Set<String> clientIds = users.keySet();
		WebSocketSession session = null;
		for (String clientId : clientIds) {
			try {
				session = users.get(clientId);
				if (session.isOpen()) {
					session.sendMessage(message);
				}
			} catch (IOException e) {
				e.printStackTrace();
				allSendSuccess = false;
			}
		}
		return allSendSuccess;
	}

	/**
	 * 获取用户标识
	 * 
	 * @param session
	 * @return
	 */
	private String getUserId(WebSocketSession session) {
		try {
			String userId = ((LoginerInfo) session.getAttributes().get("userInfo")).getUserId();
			return userId;
		} catch (Exception e) {
			return null;
		}
	}
}

源码:
React前端源码
SpringBoot后端源码

  • 4
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 18
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 18
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值