基于webScoket实现的单设备或网页登录

233 阅读4分钟

image.png

时隔了一段时间,今天来向大家简单分享个案例。

前言

前段时间因为女朋友的原因偶然接触到 公司注册 这个东西,

image.png

然后注册时需要进行闽政通验证登录,注册期间信息也填写完整了,最后也成功保存了了,她是通过向日葵远程公司电脑进行操作。

但是此时公司电脑死机了,怕内部信息被别人看到,就说你要不在你手机上进它的网址登录一下,万一可以把对面挤掉线了呢,

所以,兄弟们,重点来了 挤掉线,那么应该怎么挤呢?

这是一个问题

思路

介绍完前言,我们开始聊聊技术的架构和具体实现

对于常见的登录,我们简单画一下,如下图:

image.png

因为不需要做什么只能在一个设备登录,所以不需要携带那么多信息。

但是涉及到只能登录一个设备或者网页,那就不太一样了,具体图如下:

image.png

所以对于这种方式登录,会稍微比前一种多一捏捏的逻辑,具体分为以下几点:

  1. 登录携带 唯一标识
  2. 服务器接收到登录传递的唯一标识,进行存储到全局变量 clientsMap 中,并把唯一标识和用户信息存在一起,返回登录信息
  3. 前端接收到登录成功信息,发起 webSocket 连接,
  4. 服务器根据当前唯一标识,存储 ws 实例
  5. 之后如果还有另一个网页又发起登录流程,先获取之前存储的用户信息,从中取出唯一标识,根据唯一标识,获取 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>

页面效果

image.png

简单封装个 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>

页面效果

image.png

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 即可

clideo_editor_beeee9ad7bd44450a9c76cbaab872c18.gif

项目地址:gitee.com/warmw/simpl…

欢迎朋友们,下下来尝试尝试

image.png