时隔了一段时间,今天来向大家简单分享个案例。
前言
前段时间因为女朋友的原因偶然接触到 公司注册 这个东西,
然后注册时需要进行闽政通验证登录,注册期间信息也填写完整了,最后也成功保存了了,她是通过向日葵远程公司电脑进行操作。
但是此时公司电脑死机了,怕内部信息被别人看到,就说你要不在你手机上进它的网址登录一下,万一可以把对面挤掉线了呢,
所以,兄弟们,重点来了 挤掉线,那么应该怎么挤呢?
这是一个问题
思路
介绍完前言,我们开始聊聊技术的架构和具体实现
对于常见的登录,我们简单画一下,如下图:
因为不需要做什么只能在一个设备登录,所以不需要携带那么多信息。
但是涉及到只能登录一个设备或者网页,那就不太一样了,具体图如下:
所以对于这种方式登录,会稍微比前一种多一捏捏的逻辑,具体分为以下几点:
- 登录携带 唯一标识
- 服务器接收到登录传递的唯一标识,进行存储到全局变量 clientsMap 中,并把唯一标识和用户信息存在一起,返回登录信息
- 前端接收到登录成功信息,发起 webSocket 连接,
- 服务器根据当前唯一标识,存储 ws 实例
- 之后如果还有另一个网页又发起登录流程,先获取之前存储的用户信息,从中取出唯一标识,根据唯一标识,获取 ws 实例,并对其发送 您的账号已在其他地方登录消息,迫使它下线
具体步骤如上,由于我们实现的是简易版,所以就没有弄这么详细,接下来,我们看一下具体实现
实现
页面 1
书写 html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<p class="login-info">用户信息:<span class="user-name"></span></p>
<button class="login">登录</button>
</div>
<script src="./Socket.js"></script>
<script src="./index.js"></script>
</body>
</html>
页面效果
简单封装个 WebSocket 实例
class Socket {
constructor(port, onMessage) {
this.port = port;
this.scoket = null;
this.onMessage = onMessage;
this.create(WebSocket);
}
create(WebSocket) {
if (!this.scoket) {
this.scoket = new WebSocket(this.port);
this.scoket.onopen = this.opOpen;
this.scoket.onmessage = this.onMessage;
// this.scoket.onclose = this.onClose;
this.scoket.onerror = this.onError;
}
}
opOpen(data) {
console.log("%c connect success", "color: skyblue;font-size:16px");
}
onClose() {}
onError() {}
}
index.js
(function () {
let scoket = null;
const dlogin = document.querySelector(".login");
const duserName = document.querySelector(".user-name");
function init() {
bindEvent();
scoket = new Socket("ws://localhost:3000/wsBasic?token=client1", scoketMessage);
// fetchPage();
}
function scoketMessage({ data }) {
const config = JSON.parse(data);
if (config.code === 10001) {
duserName.textContent = config.message;
}
};
function login() {
fetch("http://localhost:3000/login", {
method: "post",
headers: {
"Content-Type": "application/json;charset=utf-8",
},
body: JSON.stringify({
username: "admin",
password: "<PASSWORD>",
code: 'client1'
}),
})
.then((res) => res.json())
.then((res) => {
duserName.textContent = res.username;
});
}
function bindEvent() {
dlogin.addEventListener("click", login);
}
init();
})();
页面 2
页面 2 和页面一 一致,只是修改了 token code,
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app">
<p>这是第二个界面</p>
<p class="login-info">用户信息:<span class="user-name"></span></p>
<button class="login">登录</button>
</div>
<script src="./Socket.js"></script>
<script>
(function () {
let scoket = null;
const dlogin = document.querySelector(".login");
const duserName = document.querySelector(".user-name");
function init() {
bindEvent();
scoket = new Socket(
"ws://localhost:3000/wsBasic?token=client2",
scoketMessage,
);
// fetchPage();
}
function scoketMessage({ data }) {
const config = JSON.parse(data);
if (config.code === 10001) {
duserName.textContent = config.message;
}
}
function login() {
fetch("http://localhost:3000/login", {
method: "post",
headers: {
"Content-Type": "application/json;charset=utf-8",
},
body: JSON.stringify({
username: "admin",
password: "<PASSWORD>",
code: "client2",
}),
})
.then((res) => res.json())
.then((res) => {
duserName.textContent = res.username;
});
}
function bindEvent() {
dlogin.addEventListener("click", login);
}
init();
})();
</script>
</body>
</html>
页面效果
serve 服务
技术栈:express + express-ws
serve服务一共就两个文件,首先是入口
const express = require("express");
// 解决传递参数
const bodyParse = require("body-parser");
// 跨域
const cors = require("cors");
// socket 服务
const expressWs = require("express-ws");
const createRouter = require("./router");
const app = express();
const router = express.Router();
expressWs(app);
app.use(cors());
// parse application/x-www-form-urlencoded
app.use(bodyParse.urlencoded({ extended: false }));
// parse application/json
app.use(bodyParse.json());
createRouter(app, router);
app.listen(3000, () => {
console.log("3000 post is running");
});
router文件
let wsInstance = null;
const clientMaps = new Map(); // 存储唯一标识和ws
const loginUserMap = new Map(); // 存储用户信息
module.exports = function createRouter(app, router) {
app.get("/", (req, res) => {
res.send("xxx");
});
app.post("/login", (req, res) => {
const { username, code } = req.body;
const cacheUser = loginUserMap.get(username);
// 登录检测,检测到已经登录,进行发送退出逻辑
if (cacheUser && cacheUser.isLogin) {
// 获取已登录设备 ws 实例
const ws = clientMaps.get(cacheUser.code);
ws.send(
JSON.stringify({
code: 10001,
quit: true,
message: "您的账号已在其他地方登录",
}),
);
}
// 重新更新
loginUserMap.set(username, {
isLogin: true,
username,
code,
});
res.json({
isLogin: true,
username,
});
});
app.ws("/wsBasic", (ws, req) => {
wsInstance = ws;
// 使用 token 作为唯一标识,将 ws 实例存入 map
clientMaps.set(req.query.token, ws);
ws.on("message", (msg) => {
// 广播消息到所有连接的客户端
clientMaps.forEach((client) => {
if (client.readyState === client.OPEN) {
client.send(msg);
}
});
});
});
};
具体效果
Client 页面使用 vscode 自带的 liveServer 即可
欢迎朋友们,下下来尝试尝试