首先,在开始开发之前,先了解一下UKEY的用户登录流程,我前面整理了一些登录的流程:

点这里查看登录流程:传送门

 

OK,了解了登录流程,我们来开始看看在vue中是怎么样进行实际的开发的。

首先你需要在导航收尾中初始化websocket的连接:

router.beforeEach((to, from, next) => {
    // 初始化后后能够监听UKEY拔插事件
  store.dispatch({
    type: "startUkey"
  });
}

补充说明:为了安全性,我们的需求是这样的:用户只有在UKEY插入的情况下才能够登录后台,用户拔出UKEY后就注销该用户。所以需要在导航守卫中初始化UKEY。

 

接下来,我们需要编写websocket逻辑处理,我将所有的websocket处理都放在vuex的action里面,下面是action的全部代码:

import { SIGN_OUT } from "@/store/modules/user/constant";

import axios from "@/modules/axios";
import route from "@/router";
import { user as userServer } from "@/modules/server-url";

var s_pnp = "";
if (!s_pnp) {
    s_pnp = new WebSocket("ws://127.0.0.1:4006/xxx","usbkey-protocol");
}


const getRandomCode = async (commit,callback) => {

  try {
    // 获取签名使用的随机数
    const data = await axios.post(userServer.getRandomCode);
    commit({
      type: "SET_RANDOM_CODE",
      playload: {
        code: data
      }
    });
    callback({
      succ_status: 3,
      msg: "获取签名随机数成功",
      data: {
        random_code_status: true,
        random_code: data
      }
    });
  } catch (err) {
    if (err && err.code) {
      callback({
        err_status: 6,
        msg: "获取签名随机数失败",
        data: {
          random_code_status: false,
          random_code: ""
        }
      })
    }
  }
};

const listenUkey = (dispatch, commit, state, request = { type: 0, pin_code: "", callback: () => {}}) => {
  try {
    var Path = ""; // 路径
    var insert_status = 0; // ukey的拔插事件会执行两次,防止第二次执行
    if (request.type != 0) { // 不是初始化流程
      let socketStatus = s_pnp.GetWebsocketStatus();
      if (socketStatus == 0) {
        setTimeout(() => {
         s_pnp.send(JSON.stringify({FunName: "ResetOrder"}));
        },500);
      } else {
         s_pnp.send(JSON.stringify({FunName: "ResetOrder"}));
      }
    }
    s_pnp.Socket_UK.onopen = function () {
      s_pnp.send(JSON.stringify({FunName: "ResetOrder"})); // 这里调用ResetOrder将计数清零,这样,消息处理处就会收到0序号的消息,通过计数及序号的方式,从而生产流程
    };

    // 在使用事件插拨时,注意,一定不要关掉Sockey,否则无法监测事件插拨
    s_pnp.onmessage = function (Msg) {
      let PnpData = JSON.parse(Msg.data);
      if (PnpData.type == "PnpEvent") { // 如果是插拨事件处理消息
        if (PnpData.IsIn) { // 监听到插入
          if (insert_status === 1) return;
          console.log("ukey插入");
          insert_status = 1;
          s_pnp.send(JSON.stringify({FunName: "ResetOrder"}));
        } else { // 监听到拔出
          if (insert_status === 2) return;
          console.log("ukey拔出");
          insert_status = 2;
          if (typeof request.callback == "function") {
            request.callback({
              err_status: 2,
              msg: NO_UKEY
            });
          }
          if (route.history.current.path == "/") return false;
          // 检测到UKEY拔出,退出登录
          return dispatch(SIGN_OUT);
        }
      }

      if (PnpData.type == "Process") { // 如果是事件处理流程
        var order = PnpData.order;
        if (state.serve_random_code.length == 0) {
          getRandomCode(commit,request.callback);
        } else {
          if (typeof request.callback == "function") {
            request.callback({
              succ_status: 3,
              msg: "获取签名随机数成功",
              data: {
                random_code_status: true,
                random_code: state.serve_random_code
              }
            });
          }
        }
        if (order == 0) {
          s_pnp.send(JSON.stringify({FunName: "FindPort",start: start})); // 查找加密锁
        } else if (order == 1) {
          if ( PnpData.LastError != 0 ) {
            if (typeof request.callback == "function") {
              request.callback({
                err_status: 2,
                msg: "未检测到UKEY"
              });
            }
            return false;
          }
          // 已插入UKEY
          Path = PnpData.return_value; // 获得返回的UK的路径
          s_pnp.send(JSON.stringify({FunName: "GetChipID",Path:Path})); // 获取锁唯一ID
        } else if (order == 2) { // 获取到锁ID
          if ( PnpData.LastError != 0 ) {
            if (typeof request.callback == "function") {
              request.callback({
                err_status: 3,
                msg: "获取锁ID失败"
              });
            }
            return false;
          }
          
          if (typeof request.callback == "function") {
            request.callback({
              succ_status: 1,
              msg: "获取锁ID成功。",
              data: {
                ukey_id: PnpData.return_value
              }
            });
          }
          // 返回设置在锁中的用户名
          s_pnp.send(JSON.stringify({FunName: "GetSm2UserName",Path:Path}));
        } else if (order == 3) { // 获取到用户身份
          if ( PnpData.LastError != 0 ) {
            if (typeof request.callback == "function") {
              request.callback({
                err_status: 4,
                msg: "获取用户名失败。"
              });
            }
          
            request.callback({
              err_status: 4,
              msg: "获取用户名失败。"
            });
            return false;
          }
          if (typeof request.callback == "function") {
            request.callback({
              succ_status: 2,
              msg: "获取用户身份成功。",
              data: {
                account: PnpData.return_value
              }
            });
          }
        }
        
        if (request.type == 1) { // 验证Pin码
          if (order == 3) {
            // 对数据进行签名,验证pin码,在内部会验证pin码,验证正确后才能够签名,验证错误后则pin码错误
              s_pnp.send(JSON.stringify({FunName: "YtSign",SignMsg:state.SignMsg,Pin:state.Pin,Path:Path}));
          } else if (order == 4) {
            if ( PnpData.LastError != 0 ) {
              request.callback({
                err_status: 5,
                msg: "Pin码验证失败。"
              });
              return false;
            }
            request.callback({
              succ_status: 4,
              msg: "签名成功",
              data: {
                autograph: PnpData.return_value
              }
            });
            commit({
              type: "SET_PIN_CODE",
              playload: {
                code: request.pin_code
              }
            });
          }
        }
      }
    };

    s_pnp.onerror = function () {
      console.log("连接错误");
    };
    s_pnp.onclose = function () {
      console.log("连接关闭");
    };
    
  } catch (e) {
    console.error(e.name + ": " + e.message);
    return false;
  }
};

export default {
  startUkey({ dispatch, commit, state }, request = { type: 0, callback: (res) => {} }) {
    // 不兼容IE10以下的浏览器
    if (navigator.userAgent.indexOf("MSIE") > 0 && !navigator.userAgent.indexOf("opera") > -1) {
      commit({
        type: "SET_IE10_UNDER",
        playload: {
          status: true,
          msg: UNDER_IE10
        }
      });
      request.callback({
        err_status: 1,
        msg: UNDER_IE10
      });
      return false;
    }
    try {

      listenUkey(dispatch, commit, state, request);

    } catch (err) {
      console.error(err);
    }
  }
};

是不是一头雾水?别急这里就给你说明一下,首先websocket的生命周期要了解一下的:

事件

事件处理程序

描述

open

Socket.onopen

连接建立时触发

message

Socket.onmessage

客户端接收服务端数据时触发

error

Socket.onerror

通信发生错误时触发

close

Socket.onclose

连接关闭时触发

我们这里主要用到的是message事件,在我的理解中message事件就是一个监听,而目标返回一次信息,就执行一次message事件,而UKEY是以轮询的方式进行通讯的,所以每次执行send函数后,都会触发message事件,每次都触发相同的函数时我们就需要根据状态来区分流程了,UKEY自身就有一套流程的记录,也就是上面代码中的order属性了,每执行一个send都会创建一个流程,order就会加一。

因为登录是需要用户输入Pin码的,不能一套流程直接走完,需要中途用户触发验证来进行验证Pin码的流程,所以这里我通过type来标识是不是用户主动触发的验证Pin码流程。

用户触发验证Pin码的代码如下:

<template>
    <div ref="signInDom" class="sign-in" >
        <el-form 
            :show-message="true"
            :model="form"
            :rules="rules"
            :ref="formName"
            label-width="15px"
            class="sign-in-form"
            @submit.native.prevent="submitForm">

            <div class="sign-in-logo">
                <img :src="logoSrc" alt="">
            </div>

            <div class="sign-in-info">
                <span>{{ tips }}</span>
            </div>            

            <div class="sign-in-form-item">
                <i class="form-input-icon icon-tubiao211"/>
                <el-form-item prop="account">
                    <el-input 
                        ref="accountInput"
                        v-model="form.account"
                        type="text" 
                        placeholder="用户名" 
                        disabled="disabled" 
                        auto-complete="off" />
                </el-form-item>
            </div>
                    
            <div class="sign-in-form-item">
                <i class="form-input-icon icon-mima1"/>
                <el-form-item prop="password">
                    <el-input  
                        ref="passwordInput"
                        v-model="form.password" 
                        type="password" 
                        placeholder="密码" 
                        auto-complete="off"
                        @keyup.enter="enterEvent"/>
                </el-form-item>
            </div>   

            <div class="sign-in-form-item">
                <i class="form-input-icon icon-mima1"/>
                <el-form-item prop="pinCode">
                    <el-input  
                        ref="pinCodeInput"
                        v-model="form.pinCode" 
                        type="password" 
                        placeholder="pin码" 
                        auto-complete="off"
                        @keyup.enter="enterEvent"/>
                </el-form-item>
            </div>
            
            <div v-if="ukey_id.length>0" class="sign-in-ukey">
                <span>当前UKEY的ID为:</span>
                <span>{{ ukey_id }}</span>
            </div>
                
        </el-form>
    </div>
</template>

<style lang="less">
@import "./index";
</style>

<script>
import logoSrc from "./images/sign-in.png";
import { mapActions, mapState, mapMutations } from "vuex";
import axios from "@/modules/axios";
import { user } from "@/modules/server-url";
import { NO_UKEY, UNDER_IE10, LOAD_UKEY_START } from "@/store/modules/ukey/constant";

export default {
  name: "SignIn",

  data() {
    const validateAccount = (rule, value, callback) => {
      if (value === "") {
        callback(new Error("用户名不能为空"));
      }
      else {
        callback();
      }
    };

    const validateCode = (rule, value, callback) => {
      if (value === "") {
        callback(new Error("验证码不能为空"));
      } else if (value.length !== 4) {
        callback(new Error("请输入4位验证码"));
      } else {
        callback();
      }
    };

    const validatePinCode = (rule, value, callback) => {
      if (value === "") {
        callback(new Error("Pin码不能为空"));
      }
      else {
        callback();
      }
    };
    
    return {
      logoSrc,
      formName: "signInForm",
      
      // 表单数据
      form: {
        account: "",
        password: "",
        pinCode: "",
        randomNum: "",
        dataSign: "",
        checked: true
      },
      
      // 验证规则
      rules: {
        account: [{ required: true, validator: validateAccount, trigger: "blur" }],
        password: [{ required: true, message: "密码不能为空", trigger: "change" }],
        pinCode: [{ required: true, message: "pin码不能为空", trigger: "change" }]
      },

      codeForm: {
        smsCode: ""
      },

      codeRules: {
        smsCode: [{ required: true, validator: validateCode, trigger: "blur" }]
      },

      pinCodeRules: {
        pinCode: [{ required: true, validator: validatePinCode, trigger: "blur" }]
      },
      
      /** 正在登陆 */
      isSignIn: false,

      /** 或验证码冷却中 */
      codeIsLoading: false,
      /** 验证码发送中 */
      codeIsSending: false,
      /** 验证码倒计时 */
      countTime: 180,
      countId: null,
      codeInnerText: "重新发送",
      
      // 展示验证码输入窗口
      showCode: false,
      tips: "",
      codeStatus: "fail",
      phone: "",

      ukey_id: "", // ukey的唯一ID
      showDownload: false, // 是否显示下载提示
      ukey_error: false,
      randomCodeLoad: true, // 签名随机数加载中
      showNotify: false // 是否显示右下角提示
    };
  },

  computed: {
    ...mapState({
      user: state => state.user
    }),

    loginStatus() {
      if (this.randomCodeLoad) {
        return true;
      }

      if (this.isSignIn) {
        return true;
      } else {
        return false;
      }
    },

    loginStatusMsg() {
      if (this.randomCodeLoad) {
        return "加载中";
      }

      if (this.isSignIn) {
        return "登录中";
      } else {
        return "登录";
      }
    },

    getTips() {
      return this.$store.state.user.signMsg;
    },
    
    getIsSignedOut: state => state.user.isSignedOut,

    /** 是否需要短信验证 */
    getSmsState: state => {
      return {
        needSmsVerify: state.user.needSmsVerify,
        codeStatus: state.user.codeStatus
      };
    }
  },

  watch: {
    getIsSignedOut(isSignedOut) {
      /** 退出登录成功 */
      if (isSignedOut) {
        this.initForminitForm();
      }
    },

    /** 设置提示信息 */
    getTips(tips) {
      this.tips = tips;
    }
  },

  mounted() {
    this.initForminitForm();
    this.LOAD_UKEY_START({type: 0, callback: this.wesocketRes});
  },

  methods: {
    ...mapActions([
      SIGN_IN,
      LOAD_UKEY_START
    ]),

    ...mapMutations([SIGN_IN_FULLFILLED]),

    /** 输入框初始化和聚焦 */
    initForminitForm() {
      const accountsHistory = getItem("signInHistory");

      if (accountsHistory) {
        this.form.account = accountsHistory.pop();

        this.focusInput("passwordInput");
      } 
      else {
        this.focusInput("accountInput");
      }

    },

    /** 表单提交 */
    submitForm() {
      if (this.isSignIn) return;
      this.isSignIn = true;

      this.$refs[this.formName].validate(async valid => {
        if (valid) {

          this.LOAD_UKEY_START({type: 1, pin_code: this.form.pinCode, callback: this.wesocketRes});
        
        } 
        else {
          this.isSignIn = false;

          return false;
        }
      });
    },

    async wesocketRes(res) {
      // console.log("wesocket返回值",res);

      if (res.err_status) {
        this.tips = res.msg;
        this.ukey_error = true;
        this.isSignIn = false;
        if (res.err_status == 2) {
          this.tips = res.msg;
          this.showDownload = true;
          this.ukey_id = "";
          this.showTipsNotify();
        }
        if (res.err_status == 6) {
          this.form.randomNum = res.data.random_code;
        }
      }

      if (res.succ_status) {
        this.tips = "";
        this.ukey_error = false;
        if (res.succ_status == 1) {
          this.ukey_id = res.data.ukey_id;
        }

        if (res.succ_status == 2) {
          this.showDownload = false;
          this.form.account = res.data.account;
        }

        if (res.succ_status == 3) { // 签名随机数
          this.randomCodeLoad = false;
          this.form.randomNum = res.data.random_code;
        }

        if (res.succ_status == 4) {
          this.form.dataSign = res.data.autograph;

        
          if (!this.judgeUkeyStatus()) return;
          // 签名成功后才能进行登录
          let formData = Object.assign({},this.form);
          delete formData.pinCode; // 不能把PIN码放在网络中传输
          let result = await this.SIGN_IN(formData, this.$router);
          this.isSignIn = false;
          if (result && result.needSmsVerify) {
            this.showCode = true;

            await this.$nextTick();
      
            // 如果需要验证码登陆,获取验证码
            this.getCode();

            this.phone = this.user.user.phone;

            const { codeStatus } = result;
      
            // 需要短信验证码  code === 6 超过短信发送次数  code === 0 正确
            if (+codeStatus === 0 || +codeStatus === 11) {
              this.codeStatus = "success";
            } else {
              this.codeStatus = "fail";
            }
          }
        }
      }
    },

    /* 检查锁状态 */
    judgeUkeyStatus() {
      if (this.ukey_error) {
        return false;
      }

      if (this.form.randomNum.length == 0) {
        return false;
      }

      if (this.form.dataSign.length == 0) {
        return false;
      }
      
      return true;
    },

    /** 重置表单 */
    resetForm() {
      if (!this.showCode && (this.form.account || this.form.password)) {
        if (this.$refs[this.formName] !== undefined) {
          this.$refs[this.formName].resetFields();
        }
      }
    },

    // 显示下载驱动提示
    showTipsNotify() {
      let that = this;
      if (that.showNotify) return false;
      that.showNotify = true;

      this.$notify({
        title: "提示",
        dangerouslyUseHTMLString: true,
        duration: 6000,
        position: "bottom-right",
        message: `<div>
                    <div style='margin-bottom: 10px;'>只有在UKEY插入并且Pin码正确后才能登陆哦。如果提示检测不到UKEY,请确认是否下载并安装了浏览器驱动。</div>
                    <div><a style='color: #03A9F4;' href='#'>立即下载驱动</a></div>
                </div>`,
        onClose: () => {
          that.showNotify = false;
        }
      });
    },

    resetCodeButton() {
      this.clearCounter();

      this.codeIsLoading = false;
    },

    clearCounter() {
      this.codeIsLoading = false;

      if (this.counterId) {
        clearInterval(this.counterId);

        this.counterId = null;
      }
    }
  }
};
</script>

 如代码所示,我使用了一个回调函数来处理UKEY函数的执行结果,提示信息或者认证状态。