WebSocket + Redis简单快速实现Web网站单设备登录功能

在这里插入图片描述
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、验证效果

打开谷歌浏览器,账号密码登录

打开火狐浏览器,模拟不同设备,账号密码登录

再看下谷歌浏览器,页面弹窗提示

在这里插入图片描述
点击确定会跳转到登录页,谷歌浏览器的账号已经被挤下线退出应用

谷歌浏览器再次登录,看下火狐浏览器,也弹窗提示,之前登录的账号也被挤下线退出应用

在这里插入图片描述
在这里插入图片描述
谢谢阅读

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
这里提供一个简单的示例代码,实现了Spring Boot和Vue.js的功能,使用WebSocket进行实时通信,并使用Redis存储历史消息。 后端代码(Spring Boot): 1. 依赖: ```xml <dependencies> <!-- Spring Boot Websocket --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <!-- Spring Boot Redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- JSON --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> </dependencies> ``` 2. 配置文件: ```yml spring: redis: host: localhost port: 6379 logging: level: org.springframework.web.socket: DEBUG ``` 3. 实体类: ```java public class Message { private String from; private String to; private String content; private Date time; // getters and setters } ``` 4. WebSocket配置: ```java @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Autowired private WebSocketHandler webSocketHandler; @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(webSocketHandler, "/chat").setAllowedOrigins("*"); } } ``` 5. WebSocket处理器: ```java @Component public class WebSocketHandler extends TextWebSocketHandler { @Autowired private RedisTemplate<String, Message> redisTemplate; private ObjectMapper objectMapper = new ObjectMapper(); private static final String KEY_PREFIX = "chat:"; @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { // 获取当前用户 String user = (String) session.getAttributes().get("user"); // 订阅Redis频道 redisTemplate.execute(new RedisCallback<Void>() { @Override public Void doInRedis(RedisConnection connection) throws DataAccessException { connection.subscribe(new MessageListener(), KEY_PREFIX + user); return null; } }); // 发送历史消息 List<Message> messages = redisTemplate.opsForList().range(KEY_PREFIX + user, 0, -1); if (messages != null && messages.size() > 0) { for (Message message : messages) { session.sendMessage(new TextMessage(objectMapper.writeValueAsString(message))); } } } @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { // 获取当前用户 String user = (String) session.getAttributes().get("user"); // 解析消息 Message msg = objectMapper.readValue(message.getPayload(), Message.class); msg.setFrom(user); msg.setTime(new Date()); // 存储到Redis redisTemplate.opsForList().rightPush(KEY_PREFIX + msg.getTo(), msg); // 发送给对方 WebSocketSession targetSession = sessions.get(msg.getTo()); if (targetSession != null && targetSession.isOpen()) { targetSession.sendMessage(new TextMessage(objectMapper.writeValueAsString(msg))); } } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { // 获取当前用户 String user = (String) session.getAttributes().get("user"); // 取消订阅Redis频道 redisTemplate.execute(new RedisCallback<Void>() { @Override public Void doInRedis(RedisConnection connection) throws DataAccessException { connection.unsubscribe(KEY_PREFIX + user); return null; } }); } private Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>(); private class MessageListener implements MessageListenerAdapter { @Override public void onMessage(Message message, byte[] pattern) { WebSocketSession session = sessions.get(message.getTo()); if (session != null && session.isOpen()) { try { session.sendMessage(new TextMessage(objectMapper.writeValueAsString(message))); } catch (Exception e) { e.printStackTrace(); } } } } } ``` 6. 控制器: ```java @RestController @RequestMapping("/api/chat") public class ChatController { @Autowired private RedisTemplate<String, Message> redisTemplate; @PostMapping("/send") public void send(@RequestBody Message message) { // 存储到Redis redisTemplate.opsForList().rightPush("chat:" + message.getFrom(), message); redisTemplate.opsForList().rightPush("chat:" + message.getTo(), message); // 发布消息 redisTemplate.convertAndSend("chat:" + message.getTo(), message); } @GetMapping("/history") public List<Message> history(String user1, String user2) { String key = "chat:" + user1 + ":" + user2; List<Message> messages = redisTemplate.opsForList().range(key, 0, -1); Collections.reverse(messages); return messages; } } ``` 前端代码(Vue.js): 1. 依赖: ```html <script src="/js/vue.min.js"></script> <script src="/js/sockjs.min.js"></script> <script src="/js/stomp.min.js"></script> <script src="/js/lodash.min.js"></script> ``` 2. HTML: ```html <div id="app"> <div> <label>当前用户:</label> <select v-model="currentUser" @change="connect"> <option v-for="user in users" :value="user">{{ user }}</option> </select> </div> <div v-if="connected"> <div> <label>对方用户:</label> <input v-model="otherUser"> </div> <div> <textarea v-model="message"></textarea> <button @click="send">发送</button> </div> <div> <ul> <li v-for="msg in messages">{{ msg.from }} -> {{ msg.to }}: {{ msg.content }}</li> </ul> </div> </div> </div> ``` 3. JavaScript: ```javascript var app = new Vue({ el: '#app', data: { users: ['user1', 'user2', 'user3'], currentUser: 'user1', otherUser: '', message: '', connected: false, messages: [] }, methods: { connect: function () { var self = this; if (self.stompClient != null) { self.stompClient.disconnect(); } var socket = new SockJS('/chat'); self.stompClient = Stomp.over(socket); self.stompClient.connect({}, function () { self.stompClient.subscribe('/user/queue/messages', function (msg) { var message = JSON.parse(msg.body); self.messages.push(message); }); self.connected = true; }, function (error) { console.log(error); }); }, send: function () { var self = this; var message = { from: self.currentUser, to: self.otherUser, content: self.message }; self.stompClient.send('/app/chat/send', {}, JSON.stringify(message)); self.message = ''; }, loadHistory: function () { var self = this; axios.get('/api/chat/history', { params: { user1: self.currentUser, user2: self.otherUser } }).then(function (response) { self.messages = response.data; }).catch(function (error) { console.log(error); }); } }, watch: { otherUser: function (newValue) { var self = this; self.loadHistory(); } } }); ``` 注意事项: 1. Redis的键名使用了前缀“chat:”,以便区分其他数据; 2. 存储历史消息和订阅消息时,使用了当前用户的名称作为频道名称; 3. 在订阅消息时,使用了内部类MessageListener处理接收到的消息,然后发送给对应的WebSocketSession; 4. 在WebSocketSession关闭时,需要取消订阅Redis频道,以免造成资源浪费; 5. 前端使用了STOMP协议进行通信,需要安装sockjs-client和stompjs库; 6. 前端通过WebSocket连接到后端时,需要指定当前用户; 7. 前端通过WebSocket接收到消息时,需要将消息添加到消息列表中; 8. 前端通过REST API加载历史消息时,需要指定当前用户和对方用户。 这是一个基础的示例,具体实现可以根据自己的需求进行调整。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

悟空码字

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值