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