UKEY開發,vue+websocket實現用戶登錄UKEY認證

首先,在開始開發之前,先了解一下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函數的執行結果,提示信息或者認證狀態。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章