vue实现web在线聊天前端-实时加密

z# 1.实现流程

  1. 首先用户导入系统,在用户页面搜索想要聊天的用户;
  2. 点击对于用户进行聊天;
  3. 跳转IM聊天页面,在该页面首先轮询请求服务器获取聊天密钥,并判断对方是否在线(具体实现在后端),若对方在线且双方都已获取到密钥,就与服务器建立websocket双向通信的长连接,用户发送与接收的消息都使用上面分配的密钥进行加密解密;
  4. 目前若有一方断开连接则密钥不进行保存,需要重新连接重新重复上面流程,且目前为添加心跳检测,让对方判断是否在线。
  5. 加密方式:消息加密需使用AES加密,加密模式为CBC,填充使用pkcs5padding,数据块为128位,密码和偏移量为获取到的密钥,输出为base64,字符集为utf8。

2.代码

<template>
  <div style="padding: 10px; margin-bottom: 50px">
    <el-button
      type="primary"
      size="mini"
      @click.prevent="handleMenuSelect('/User')"
      >用户</el-button
    >
    <el-row v-loading="loading" element-loading-text="正在连接....">
      <el-col :span="16">
        <div
          style="
            width: 800px;
            margin: 0 auto;
            background-color: white;
            border-radius: 5px;
            box-shadow: 0 0 10px #ccc;
          "
        >
          <div style="text-align: center; line-height: 50px">
            ({{ chatUser }})
          </div>
          <div
            style="height: 350px; overflow: auto; border-top: 1px solid #ccc"
            v-html="content"
          ></div>
          <div style="height: 200px">
            <textarea
              v-model="text"
              style="
                height: 160px;
                width: 100%;
                padding: 20px;
                border: none;
                border-top: 1px solid #ccc;
                border-bottom: 1px solid #ccc;
                outline: none;
              "
            ></textarea>
            <div style="text-align: right; padding-right: 10px">
              <el-button type="primary" size="mini" @click="send"
                >发送</el-button
              >
            </div>
          </div>
        </div>
      </el-col>
    </el-row>
  </div>
</template>
<script>
import { ref, onMounted, inject, onUnmounted } from "vue";
import { getImKeyService } from "@/api/im.js";
import router from "@/router/index.js";
import * as crypto from "crypto";
import CryptoJS from "crypto-js";
import { ElLoading } from "element-plus";
import Cookies from "js-cookie";
export default {
  data() {
    return {
      circleUrl:
        "https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png",
      user: {},
      isCollapse: false,
      users: [{ username: "admin" }, { username: "zhang" }],
      chatUser: "2002",
      text: "",
      messages: [],
      session: "",
      loading: ref(true),
      imKey: "testimkey",
      socket: null,
      content: "",
      to_user_id: 0,
      to_user_name: "",
      cnt: 0,
      intervalId: null,
      tokenData: {
        token: localStorage.getItem("token"),
        ip: localStorage.getItem("ip"),
        userId: localStorage.getItem("userId"),
        username: localStorage.getItem("username"),
        to_user_id: this.to_user_id,
      },
      intervalId: ref(null),
      timerId: null, // 用于存储定时器的ID
    };
  },
  methods: {
    async send() {
      if (!this.text) {
        this.$message({ type: "warning", message: "请输入内容" });
      } else {
        if (typeof WebSocket == "undefined") {
          console.log("您的浏览器不支持WebSocket");
        } else {
          // 组装待发送的消息 json
          // {"type":"msg","data":"hello","to_user_id":1,"from_user_id":2002,"session":"11917957"}
          //   let message = {
          //     type: "msg",
          //     data: "hello",
          //     to_user_id: this.to_user_id,
          //     from_user_id: this.tokenData.userId,
          //     session: this.session,
          //   };
          //   let json=JSON.stringify(message)
          //   var aesEnc = await this.aesEncrypt(
          //     this.text,
          //     this.imKey,
          //     this.session
          //   );
          var aesEnc = await this.aesEncrypt(this.text, this.imKey, this.imKey);
          let data =
            '{"type":"msg","data":"' +
            aesEnc +
            '","to_user_id":' +
            this.to_user_id +
            ',"from_user_id":' +
            this.tokenData.userId +
            ',"session":"' +
            this.session +
            '"}';
          this.socket.send(data); // 将组装好的json发送给服务端,由服务端进行转发
          this.messages.push(JSON.parse(data));
          // 构建消息内容,本人消息
          this.createContent(null, this.tokenData.username, this.text);
          this.text = "";
        }
      }
    },
    startInterval() {
      this.timerId = setInterval(() => {
        this.getIMKey();
      }, 1000); // 每秒(1000毫秒)执行一次
    },
    handleMenuSelect(val) {
      router.push(val);
    },
    stopInterval() {
      clearInterval(this.timerId);
    },
    async getIMKey() {
      let result = {};
      try {
        let req = {
          to_user_id: this.to_user_id,
          token: this.tokenData.token,
        };
        result = await getImKeyService(req);
      } catch (e) {
        console.log(e);
      }
      let data = result.data;
      if (result.code != 0) {
        this.cnt++;
        if (this.cnt > 10) {
          //暂停定时器
          this.stopInterval();
          alert("连接失败,请重试!");
          router.push("/user");
        }
        return;
      }
      try {
        if (data.is_read == 1) {
          // 读取到了新的消息
          this.imKey = data.im_key;
          this.session = data.im_session;
          localStorage.setItem("imkey_" + this.to_user_id, this.imKey);
          //记录过期时间
          localStorage.setItem(
            "imkey_" + this.to_user_id + "_expire",
            data.expire
          );
          this.session = data.im_session;
          //暂停定时器
          this.stopInterval();
          this.connectWebSocket();
        } else {
          this.cnt++;
          if (this.cnt > 30) {
            //暂停定时器
            this.stopInterval();
            confirm("连接失败,请重试!");
            router.push("/user");
          }
        }
      } catch (e) {
        console.log(e);
      }
    },
    createContent(remoteUser, nowUser, text) {
      // 这个方法是用来将 json的聊天消息数据转换成 html的。
      var html;
      // 当前用户消息
      if (nowUser) {
        // nowUser 表示是否显示当前用户发送的聊天消息,绿色气泡
        html =
          '<div class="el-row" style="padding: 5px 0">\n' +
          '  <div class="el-col el-col-22" style="text-align: right; padding-right: 10px">\n' +
          '    <div class="tip left">' +
          text +
          "</div>\n" +
          "  </div>\n" +
          '  <div class="el-col el-col-2">\n' +
          '  <span class="el-avatar el-avatar--circle" style="height: 40px; width: 40px; line-height: 40px;">\n' +
          '    <img src="https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png" style="object-fit: cover;">\n' +
          "  </span>\n" +
          "  </div>\n" +
          "</div>";
      } else if (remoteUser) {
        // remoteUser表示远程用户聊天消息,蓝色的气泡
        html =
          '<div class="el-row" style="padding: 5px 0">\n' +
          '  <div class="el-col el-col-2" style="text-align: right">\n' +
          '  <span class="el-avatar el-avatar--circle" style="height: 40px; width: 40px; line-height: 40px;">\n' +
          '    <img src="https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png" style="object-fit: cover;">\n' +
          "  </span>\n" +
          "  </div>\n" +
          '  <div class="el-col el-col-22" style="text-align: left; padding-left: 10px">\n' +
          '    <div class="tip right">' +
          text +
          "</div>\n" +
          "  </div>\n" +
          "</div>";
      }
      this.content += html;
    },
    aesEncrypt(text, password, iv) {
      // CryptoJS使用WordArray而不是Buffer,所以我们需要将密码和IV转换为WordArray
      const passwordWordArray = CryptoJS.enc.Utf8.parse(password);
      const ivWordArray = CryptoJS.enc.Utf8.parse(iv);

      // 使用CryptoJS创建AES加密实例
      const cipher = CryptoJS.AES.encrypt(
        CryptoJS.enc.Utf8.parse(text), // 加密的数据
        passwordWordArray, // 密钥
        {
          iv: ivWordArray, // 初始化向量
          mode: CryptoJS.mode.CBC, // 加密模式
          padding: CryptoJS.pad.Pkcs7, // 填充方式
        }
      );

      // CryptoJS默认返回的是CipherParams对象,我们需要将其转换为Base64字符串
      const encrypted = cipher.toString();

      return encrypted;
    },
    decryptAES(encryptedData, secretKey, iv) {
      // 确保密钥和偏移量(IV)是WordArray对象
      const key = CryptoJS.enc.Utf8.parse(secretKey);
      const ivData = CryptoJS.enc.Utf8.parse(iv);

      // Base64 解码加密数据
      // 注意:CryptoJS.enc.Base64.parse 实际上是用于将WordArray转换为Base64字符串的,
      // 对于从Base64字符串解码,应该使用 CryptoJS.enc.Base64.parse(CryptoJS.enc.Base64.stringify(someWordArray)),
      // 但在这里我们直接使用字符串作为输入,因为encryptedData已经是Base64编码的字符串。
      try {
        const decrypted = CryptoJS.AES.decrypt(encryptedData, key, {
          iv: ivData,
          mode: CryptoJS.mode.CBC,
          padding: CryptoJS.pad.Pkcs7,
        });

        // 检查解密是否成功
        if (decrypted.sigBytes === 0) {
          throw new Error("Decryption failed. Invalid data or password.");
        }

        // 将解密后的 WordArray 转换为 UTF-8 字符串
        const decryptedText = decrypted.toString(CryptoJS.enc.Utf8);
        return decryptedText;
      } catch (error) {
        // 捕获并处理异常
        console.error("Decryption error:", error);
        throw error; // 或者返回一个错误消息或空字符串等
      }
    },
    connectWebSocket() {
      // 连接WebSocket
      let _this = this;
      if (typeof WebSocket == "undefined") {
        console.log("浏览器不支持WebSocket");
      } else {
        console.log("浏览器支持WebSocket");
        let socketUrl =
          "wss://gep.ljsea.top/im/ws?to_user_id=" +
          this.to_user_id +
          "&token=" +
          this.tokenData.token;

        // console.log("socketUrl:", socketUrl);
        if (this.socket != null) {
          this.socket.close();
          this.socket = null;
        }
        // 开启一个websocket服务
        this.socket = new WebSocket(socketUrl);
        //打开事件
        this.socket.onopen = function () {
          confirm("连接成功");
          this.loading = false;
        };
        this.socket.onerror = (error) => {
          console.error("WebSocket Error:", error);
        };
        //关闭事件
        this.socket.onclose = function () {
          alert("连接已关闭!");
          router.push("/user");
        };
        //  浏览器端收消息,获得从服务端发送过来的文本消息
        this.socket.onmessage = async function (msg) {
          //console.log("收到数据====" + msg.data);
          let data = JSON.parse(msg.data); // 对收到的json数据进行解析, 类似这样的:
          // 如果服务器端发送过来的json数据
          if (data.type == "msg") {  // 如果是消息类型,解密消息内容
            data.data = await _this.decryptAES(
              data.data,
              _this.imKey,
              _this.imKey
            );
            _this.messages.push(data);
            //console.log("收到数据====" + msg.data);
            // 构建消息内容
            _this.createContent(_this.to_user_name, null, data.data);
          }
        };
      }
    },
  },
  // 生命周期钩子,在组件挂载完成后被调用
  mounted() {
    this.to_user_id = localStorage.getItem("to_user_id");
    this.to_user_name = localStorage.getItem("to_user_name");
    this.chatUser = this.to_user_name;
    //console.log("to_user_id:", this.to_user_id, this.to_user_name);
    this.startInterval();
    //this.connectWebSocket();
  },
  // 生命周期钩子,在组件卸载之前被调用
  onUnmounted() {
    this.stopInterval();
    this.socket.close();
    this.loading = false;
  },
};
</script>
  <style>
.tip {
  color: white;
  text-align: center;
  border-radius: 10px;
  font-family: sans-serif;
  padding: 10px;
  width: auto;
  display: inline-block !important;
  display: inline;
}
.right {
  background-color: deepskyblue;
}
.left {
  background-color: forestgreen;
}
</style>

3.实现效果

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇