前端Vue项目——登录页面实现
一、geetest滑动验证
geetest官方文档地址:https://docs.geetest.com/
产品——极速验证:基于深度学习的人机识别应用。极验「行为验证」是一项可以帮助你的网站与APP识别与拦截机器程序批量自动化操作的SaaS应用。它是由极验开发的新一代人机验证产品,它不基于传统“问题-答案”的检测模式,而是通过利用深度学习对验证过程中产生的行为数据进行高维分析,发现人机行为模式与行为特征的差异,更加精准地区分人机行为。
1、web部署介绍
客户端官方文档:https://docs.geetest.com/install/deploy/client/web/
(1)引入初始化函数
通过引入 gt.js 文件,引入 initGeetest 初始化函数。
<script src="gt.js"></script>
注意:行为验证要求初始化在业务页面加载时同时初始化,否则验证无法读取用户在业务页面操作的行为数据,导致验证策略失效。
这里的 gt.js 文件,它用于加载对应的验证JS库。在每个后端语言的sdk中都存有一份,开发者部署到实际环境需要将该文件复制到相应的项目中使用。
之前该文件地址是 https://static.geetest.com/static/tools/gt.js ,改为存放在用户的项目中,防止静态服务器出问题无法加载该文件。
(2)调用初始化函数初始化
使用初始化函数 initGeetest
初始化后,回调的第一个参数即是验证实例,它的第二个参数是一个回调,如下代码所示。
ajax({ url: "API1接口(详见服务端部署)", type: "get", dataType: "json", success: function (data) { //请检测data的数据结构, 保证data.gt, data.challenge, data.success有值 initGeetest({ // 以下配置参数来自服务端 SDK gt: data.gt, challenge: data.challenge, offline: !data.success, new_captcha: true }, function (captchaObj) { // 这里可以调用验证实例 captchaObj 的实例方法 }) } })
注: 对于同一个页面存在多个验证码场景的初始化,需要每个验证码场景调用 initGeetest 方法单独进行初始化;如果一个场景下有多个验证入口,需要进行多次初始化。
(3)product参数设置二级验证
在行为验证中,绝大多数真实用户仅需点击一下即可通过验证。但是考虑到实际风险情况,被行为验证判定为有风险的请求,会进入下个阶段的验证。此时,行为验证提供了弹出二级验证的交互样式,方便用户兼容自己本身的界面。这里以float浮动式验证为例:
initGeetest({ // 省略必须的配置参数 product: 'float' }, function (captchaObj) { captchaObj.appendTo("#captchaBox"); //将验证按钮插入到宿主页面中captchaBox元素内 captchaObj.onReady(function(){ //your code }).onSuccess(function(){ //your code }).onError(function(){ //your code }) });
2、vue项目Login页面geetest实现
观察各大网站使用了geetest的login页面接口信息,可以发现请求发回的数据中包含gt、challenge、success的值。在单页面应用Login.vue中实现geetest验证。
(1)在项目中全局引入geetest
在前端项目中创建/src/global/gt.js文件,写入文件地址 https://static.geetest.com/static/tools/gt.js 的内容。
在/src/main.js中全局引入gt.js文件:
import '../static/global/gt.js'
(2)添加geetest接口
在 /src/restful/api.js 中添加geetest接口如下所示:
// geetest接口 export const geetest = ()=>{ return Axios.get('captcha_check/').then(res=>res.data); }
(3)配置getGeetest方法
在getGeetest方法中调用geetest初始化函数初始化:
<script> export default { name: 'Login', data(){ return { username: "", password: "" } }, methods:{ getGeetest(){ this.$http.geetest() .then(res=>{ console.log(res); let data = res.data; //请检测data的数据结构, 保证data.gt, data.challenge, data.success有值 initGeetest({ // 以下配置参数来自服务端 SDK gt: data.gt, // 验证id,极验后台申请得到 challenge: data.challenge, // 验证流水号,后服务端SDK向极验服务器申请得到 offline: !data.success, // 极验API服务器是否宕机(即处于fallback状态) new_captcha: true, // 宕机情况下使用,表示验证是3.0还是2.0,3.0的sdk该字段为true product: popup, // 弹出式展现 with: '100%' // 默认宽度300px }, function (captchaObj) { // 这里可以调用验证实例 captchaObj 的实例方法 captchaObj.appendTo("#geetest"); //将验证按钮插入到宿主页面中captchaObj元素内 captchaObj.onReady(function(){ //your code }).onSuccess(function(){ //your code }).onError(function(){ //your code }) }) }) .catch(err=>{ console.log(err); }) } }, created() { this.getGeetest(); } }; </script>
(4)appendTo(position)
appendTo
方法用于将验证按钮插到宿主页面,使其显示在页面上。接受的参数可以是 id 选择器(例如 #captcha-box
),或者 DOM 元素对象。
<div id="captcha-box"></div> // 方式一:传入id选择器 <script> initGeetest({ // 省略配置参数 }, function (captchaObj) { captchaObj.appendTo('#captcha-box'); // 省略其他方法的调用 }); </script> // 方式二:传入DOM元素 <script> var captchaBox = document.getElementById('#captcha-box'); initGeetest({ // 省略配置参数 }, function (captchaObj) { captchaObj.appendTo(captchaBox); // 省略其他方法的调用 }); </script>
(5)onReady(callback)
监听验证按钮的 DOM 生成完毕事件。参数 callback
为函数类型。
<div id="captcha-box"> <div id="loading-tip">加载中,请稍后...</div> </div> <script> initGeetest({ // 省略配置参数 }, function (captchaObj) { captchaObj.appendto('#captcha-box'); // 省略其他方法的调用 captchaObj.onReady(function () { // DOM 准备好后,隐藏 #loading-tip 元素 // 仅作示例用,用您适合的方式隐藏即可 document.getElementById('loading-tip').style.display = 'none'; }); }); </script>
(6)geetest校验显示效果
显示效果如下所示:
二、二次验证数据处理和登录实现
1、getValidate()方法二次校验
获取用户进行成功验证(onSuccess
)所得到的结果,该结果用于进行服务端 SDK 进行二次验证。getValidate
方法返回一个对象,该对象包含 geetest_challenge
,geetest_validate
,geetest_seccode
字段。
export default { name: 'Login', data(){ return { username: "", password: "", validateResult: {} // 验证成功后返回的结果,用于服务端sdk二次验证 } }, methods:{ getGeetest(){ this.$http.geetest() .then(res=>{ console.log(res); let data = res.data; // 将当前组件this对象赋值给 _this var _this = this; // 函数的调用方式决定了this的值。this不能在执行期间被赋值,并且在每次函数被调用时this的值也可能会不同。 //请检测data的数据结构, 保证data.gt, data.challenge, data.success有值 initGeetest({ // 以下配置参数来自服务端 SDK gt: data.gt, // 验证id,极验后台申请得到 challenge: data.challenge, // 验证流水号,后服务端SDK向极验服务器申请得到 offline: !data.success, // 极验API服务器是否宕机(即处于fallback状态) new_captcha: true, // 宕机情况下使用,表示验证是3.0还是2.0,3.0的sdk该字段为true product: popup, // 弹出式展现 with: '100%' // 默认宽度300px }, function (captchaObj) { // 这里可以调用验证实例 captchaObj 的实例方法 captchaObj.appendTo("#geetest"); //将验证按钮插入到宿主页面中captchaObj元素内 captchaObj.onReady(function(){ //your code }).onSuccess(function(){ //your code console.log(captchaObj); var result = captchaObj.getValidate(); _this.validateResult = result; }).onError(function(){ //your code }) }) }) .catch(err=>{ console.log(err); }) }
2、配置登录接口
在 src/restful/api.js 文件中配置登录接口:
// 登录接口 export const userLogin = (params)=>{ // 这个参数至少有5个字段,username,password,geetest_challenge,geetest_validate,geetest_seccode return Axios.post('account/login/', params).then(res=>res.data); };
3、登录事件
在登录按钮上绑定登录事件:
<button class="login_btn" @click="loginHandler">登录</button> <p class="go_login" >没有账号 <span>立即注册</span></p>
添加loginHandler方法:
methods:{ loginHandler(){ let params = { // 5个字段 username: this.username, password: this.password, geet_challenge: this.validateResult.geet_challenge, geet_validate: this.validateResult.geet_validate, geet_seccode: this.validateResult.geet_seccode }; this.$http.userLogin(params) .then(res=>{ console.log(res); }) .catch(err=>{ console.log(err); }) },
在页面登录查看控制台输出的data信息:
4、登录数据解析
在二次验证成功后,通过编程式导航跳转到Home组件,显示网站首页,但是跳转时需要携带验证时获取的登录数据信息。
这里使用localstorage来存储登录信息:
methods:{ loginHandler(){ let params = { // 5个字段 username: this.username, password: this.password, geet_challenge: this.validateResult.geet_challenge, geet_validate: this.validateResult.geet_validate, geet_seccode: this.validateResult.geet_seccode }; this.$http.userLogin(params) .then(res=>{ console.log(res); if (res.error_no === 0){ // 验证成功 this.$router.push({ // 路由跳转到Home组件 name: "Home" }); localStorage.setItem('access_token', res.data.access_token); // token值判断是否登录 localStorage.setItem('username', res.data.username); // 用户名 localStorage.setItem('avatar', res.data.avatar); // 用户头像 } }) .catch(err=>{ console.log(err); }) },
查看控制台Application中显示的Local Storage信息:
只读的localStorage
属性允许你访问一个Document
源(origin)的对象 Storage
;其存储的数据能在跨浏览器会话保留。localStorage
类似 sessionStorage
,但其区别在于:存储在 localStorage
的数据可以长期保留;而当页面会话结束——也就是说,当页面被关闭时,存储在 sessionStorage
的数据会被清除 。
无论数据存储在 localStorage
还是 sessionStorage
,它们都特定于页面的协议。
另外,localStorage
中的键值对总是以字符串的形式存储。 (需要注意, 和js对象相比, 键值对总是以字符串的形式存储意味着数值类型会自动转化为字符串类型).
5、用户登录后组件通信问题
登录点击后,页面跳转至网站首页(Home组件),同时也使用了localStorage存储了需要保存的对象。但是要想将存储的用户名和头像展示到页首(Header组件)却无法完成。Home组件和Header组件没有关联。需要使用 Vuex 插件,集中式存储管理应该的所有组件的状态,并以相应的规则保证状态以一种可预测的方式变化。
三、vue-cli项目中集成Vuex
1、npm安装vuex
vue-cli构建的项目中不包含vuex,手动引入:
$ npm install vuex -S
2、vuex入口文件配置
安装 Vuex 后,创建 src/store 目录,再创建 src/store/index.js 文件,用作组装模块并导出store。
import Vue from 'vue' import Vuex from 'vuex' //引入vuex // 使用插件 Vue.use(Vuex); //创建vuex中的store对象 let store = new Vuex.Store({ // 三大将 state:{ userInfo: {} }, // 修改state的唯一方法:提交mutations mutations:{ getUserInfo(state, user){ state.userInfo = user; } }, actions:{ getUserInfo({commit}, user){ commit('getUserInfo', user) } } }); export default store; // 抛出store对象
3、main.js中引入vuex的store
import Vue from 'vue' import Vuex from 'vuex' import App from './App' import router from './router' import ElementUI from 'element-ui' import 'element-ui/lib/theme-chalk/index.css' import '../static/global/global.css' import '../static/global/gt.js' Vue.use(ElementUI); Vue.use(Vuex); import * as api from './restful/api' console.log(api); Vue.prototype.$http = api; // store引入 import store from '../src/store/index' Vue.config.productionTip = false; new Vue({ el: '#app', router, store, // store对象挂载到vue实例中 components: { App }, template: '<App/>' });
将store对象挂载到vue实例中后,各个组件中可以通过 this.$store获取当前的store对象。
因此可以通过 this.$store.state.userInfo来获取当前的用户对象。
4、Login组件中将用户信息保存到state中
前面已经在Login组件中写了 loginHandler 方法,来处理登录事件。
验证成功后将用户信息保存到localStorage中,这里还要将用户信息保存到 store对象的 userInfo 字段中。改写如下所示:
methods:{ loginHandler(){ let params = { // 5个字段 username: this.username, password: this.password, geet_challenge: this.validateResult.geet_challenge, geet_validate: this.validateResult.geet_validate, geet_seccode: this.validateResult.geet_seccode }; this.$http.userLogin(params) .then(res=>{ console.log(res); if (res.error_no === 0){ // 验证成功 this.$router.push({ // 路由跳转到Home组件 name: "Home" }); localStorage.setItem('access_token', res.data.access_token); // token值判断是否登录 localStorage.setItem('username', res.data.username); // 用户名 localStorage.setItem('avatar', res.data.avatar); // 用户头像 localStorage.setItem('shop_cart_num', res.data.shop_cart_num); // 购物车数量 // dispatch action的行为 this.$store.dispatch('getUserInfo', res.data); } }) .catch(err=>{ console.log(err); }) },
5、Header组件中监听用户信息更新信息显示
页首组件在用户没有登录是会显示登录、注册按钮。在用户注册后应显示用户名及用户头像等信息。
(1)实时监听userInfo数据
<script> export default { name: 'LuffyHeader', data() { return { headerList: [ {id: '1', name: 'Home', title: '首页'}, {id: '2', name: 'Course', title: '免费课程'}, {id: '3', name: 'LightCourse', title: '轻课'}, {id: '4', name: 'Micro', title: '学位课程'} ] } }, computed: { userInfo(){ return this.$store.state.userInfo; } } }; </script>
methods、watch、computed对比:
- computed 属性的结果会被缓存,除非依赖的响应式属性变化才会重新计算。主要当作属性来使用;
- methods 方法表示一个具体的操作,主要书写业务逻辑;
- watch 一个对象,键是需要观察的表达式,值是对应回调函数。主要用来监听某些特定数据的变化,从而进行某些具体的业务逻辑操作;可以看作是 computed 和 methods 的结合体;
(2)template中修改登录前后模板显示
判断userInfo.access_token是否有值,没有值显示登录/注册,有值则显示登录信息。
<div class="nav-right" v-if="userInfo.access_token"> <span class = 'el-dropdown-link'>学习中心</span> <span class="user">{{userInfo.username}}</span> <img :src="userInfo.avatar" alt=""> <ul class="my_account"> <li>我的账户<i>></i></li> <li>我的订单<i>></i></li> <li>我的优惠券<i>></i></li> <li>我的消息<span class="msg">({{userInfo.notice_num}})</span><i>></i></li> <li>购物车<span class="count">({{userInfo.shop_cart_num}})</span></li> <li>退出<i>></i></li> </ul> </div> <!-- </el-dropdown> --> <div class="nav-right" v-else> <span>登录</span> | <span>注册</span> </div>
(3)登录后页面显示如下所示
6、鼠标悬浮显示/隐藏下拉框
使用v-show来控制无序列表 <ul> 标签的显示和隐藏。
(1)设置 ul 标签默认隐藏
<template> <!-- 代码略 --> <div class="nav-right" v-if="userInfo.access_token"> <span class = 'el-dropdown-link'>学习中心</span> <span class="user">{{userInfo.username}}</span> <img :src="userInfo.avatar" alt=""> <ul class="my_account" v-show="isShow"> <li>我的账户<i>></i></li> <li>我的订单<i>></i></li> <li>我的优惠券<i>></i></li> <li>我的消息<span class="msg">({{userInfo.notice_num}})</span><i>></i></li> <li>购物车<span class="count">({{userInfo.shop_cart_num}})</span></li> <li>退出<i>></i></li> </ul> </div> </template> <script> export default { name: 'LuffyHeader', data() { return { headerList: [ // 代码略 ], isShow: false // 默认隐藏 } }, computed: { userInfo(){ return this.$store.state.userInfo; } } }; </script>
(2)鼠标悬浮显示ul
isShow默认值为false,即默认隐藏。鼠标移入触发enterHandler事件后ul 显示,鼠标移出触发leaveHandler事件后ul隐藏。
<template> <!--代码略--> <div class="nav-right" v-if="userInfo.access_token" @mouseenter="enterHandler" @mouseleave="leaveHandler"> <span class = 'el-dropdown-link'>学习中心</span> <span class="user">{{userInfo.username}}</span> <img :src="userInfo.avatar" alt=""> <ul class="my_account" v-show="isShow"> <li>我的账户<i>></i></li> <li>我的订单<i>></i></li> <li>我的优惠券<i>></i></li> <li>我的消息<span class="msg">({{userInfo.notice_num}})</span><i>></i></li> <li>购物车<span class="count">({{userInfo.shop_cart_num}})</span></li> <li>退出<i>></i></li> </ul> </div> <!-- </el-dropdown> --> <div class="nav-right" v-else> <span>登录</span> | <span>注册</span> </div> <!--代码略--> </template> <script> export default { name: 'LuffyHeader', data() { return { headerList: [ // 代码略 ], isShow: false // 默认隐藏 } }, methods: { enterHandler() { this.isShow = true; }, leaveHandler() { this.isShow = false; } }, computed: { userInfo(){ return this.$store.state.userInfo; } } }; </script>
四、全局守卫保持用户始终登录
前面实现了用户登录,Header显示用户登录信息。但是如果刷新页面或者跳转到导航栏其他页面。会发现Header显示的不是用户登录信息,而是 登录/注册。
因此需要使用Vue-Router 的导航守卫来保存用户始终登录。
1、在main.js中引入路由全局守卫
// store引入 import store from '../src/store/index' // 路由全局守卫 router.beforeEach((to, from, next)=>{ console.log(to); console.log(from); next(); // 确保要调用 next 方法,否则钩子就不会被 resolved(发生阻塞) }); Vue.config.productionTip = false;
控制台查看to,from打印信息:
from是从哪里来的路由(当前导航要离开的路由),to是到哪里去的路由(即将要进入的目标路由对象)。
2、分发Action获取用户信息
在main.js 中读取localStorage中存储的用户信息。通过 store.dispatch
方法触发Action中的 getUserInfo 方法。
// 路由全局守卫 router.beforeEach((to, from, next)=>{ // console.log(to); // console.log(from); if(localStorage.getItem('access_token')){ // 用户登录过了 let user = { // 获取用户信息 access_token: localStorage.getItem('access_token'), username: localStorage.getItem('username'), avatar: localStorage.getItem('avatar'), shop_cart_num: localStorage.getItem('shop_cart_num') }; // 通过dispatch调用action中方法getUserInfo store.dispatch('getUserInfo', user); } next(); // 确保要调用 next 方法,否则钩子就不会被 resolved(发生阻塞) });
提交mutation后,实现更改 Vuex 的 store 的 state。
Header组件中 compute 监听到userInfo变化,因此刷新页面或者跳转到导航栏其他页面时,都会正常显示用户登录信息。