首先,在开始开发之前,先了解一下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函数的执行结果,提示信息或者认证状态。