目录
01: 构建登录模块基础UI结构
02: 表单校验实现原理与方案分析
表单校验的实现原理
自定义表单校验方案分析
文章中的方案实现
03: 基于 vee-validate 实现普适的表单校验
04: 什么是人类行为验证?它的目的、实现原理、构建方案分别是什么?
什么是人类行为验证
目的
它的实现原理是什么?
我们如何在项目中使用它
05: 构建人类行为验证模块
06: 用户登录行为处理
07: 用户信息获取行为
08: 退出登录操作
09: token 超时处理
10: 注册页面基本样式处理
11: 处理注册行为
12: 总结
01: 构建登录模块基础UI结构
- src/views
- - login-register
- - - components
- - - - header.vue
- - - login
- - - - index.vue
// src/views/login-register/components/header.vue<template><!-- 头部图标:PC端 --><div class="hidden pt-5 h-8 xl:block"><imgv-lazyclass="m-auto"src="https://res.lgdsunday.club/signlogo.png"alt=""/></div><!-- 头部图标:移动端 --><div class="h-[111px] xl:hidden"><imgv-lazyclass="dark:hidden"src="https://res.lgdsunday.club/login-bg.png"alt=""/><imgv-lazyclass="h-5 absolute top-[5%] left-[50%] translate-x-[-50%]"src="https://m.imooc.com/static/wap/static/common/img/logo-small@2x.png"alt=""srcset=""/></div>
</template>
// src/views/login-register/login/index.vue<template><divclass="relative h-screen bg-white dark:bg-zinc-800 text-center xl:bg-zinc-200"><!-- 头部图标:PC端 --><header-vue></header-vue><!-- 表单区 --><divclass="block px-3 mt-4 dark:bg-zinc-800 xl:bg-white xl:w-[388px] xl:dark:bg-zinc-900 xl:m-auto xl:mt-8 xl:py-4 xl:rounded-sm xl:shadow-lg"><h3class="mb-2 font-semibold text-base text-main dark:text-zinc-300 hidden xl:block">账号登录</h3><!-- 表单 --><vee-form @submit="onLoginHandler"><vee-fieldclass="dark:bg-zinc-800 dark:text-zinc-400 border-b-zinc-400 border-b-[1px] w-full outline-0 pb-1 px-1 text-base focus:border-b-main dark:focus:border-b-zinc-200 xl:dark:bg-zinc-900"name="username":rules="validateUsername"type="text"placeholder="用户名"autocomplete="on"v-model="loginForm.username"/><vee-error-messageclass="text-sm text-red-600 block mt-0.5 text-left"name="username"></vee-error-message><vee-fieldclass="dark:bg-zinc-800 dark:text-zinc-400 border-b-zinc-400 border-b-[1px] w-full outline-0 pb-1 px-1 text-base focus:border-b-main dark:focus:border-b-zinc-200 xl:dark:bg-zinc-900"name="password":rules="validatePassword"type="password"placeholder="密码"autocomplete="on"v-model="loginForm.password"/><vee-error-messageclass="text-sm text-red-600 block mt-0.5 text-left"name="password"></vee-error-message><div class="pt-1 pb-3 leading-[0px] text-right"><aclass="inline-block p-1 text-zinc-400 text-right dark:text-zinc-600 hover:text-zinc-600 dark:hover:text-zinc-400 text-sm duration-400 cursor-pointer"@click="onToRegister">去注册</a></div><m-buttonclass="w-full dark:bg-zinc-900 xl:dark:bg-zinc-800":loading="loading":isActiveAnim="false">登录</m-button></vee-form><div class="flex justify-around mt-4"><!-- QQ --><qq-login-vue></qq-login-vue><!-- 微信 --><wx-login-vue></wx-login-vue></div></div><!-- 人类行为验证模块 --><slider-captcha-vuev-if="isSliderCaptchaVisible"@close="isSliderCaptchaVisible = false"@success="onCaptchaSuccess"></slider-captcha-vue></div>
</template><script>
export default {name: 'login'
}
</script><script setup>
import headerVue from '../components/header.vue'
import sliderCaptchaVue from './slider-captcha.vue'
import {Form as VeeForm,Field as VeeField,ErrorMessage as VeeErrorMessage
} from 'vee-validate'
import { validateUsername, validatePassword } from '../validate'
import { ref } from 'vue'
import { useStore } from 'vuex'
import { useRouter } from 'vue-router'
import { LOGIN_TYPE_USERNAME } from '@/constants'
import qqLoginVue from './qq-login.vue'
import wxLoginVue from './weixin-login.vue'const store = useStore()
const router = useRouter()// 控制 sliderCaptcha 展示
const isSliderCaptchaVisible = ref(false)/*** 登录触发*/
const onLoginHandler = () => {isSliderCaptchaVisible.value = true
}/*** 人类行为验证通过*/
const onCaptchaSuccess = async () => {isSliderCaptchaVisible.value = false// 登录操作onLogin()
}// 登录时的 loading
const loading = ref(false)
// 用户输入的用户名和密码
const loginForm = ref({username: '',password: ''
})
/*** 用户登录行为*/
const onLogin = async () => {loading.value = true// 执行登录操作try {await store.dispatch('user/login', {...loginForm.value,loginType: LOGIN_TYPE_USERNAME})} finally {loading.value = false}router.push('/')
}/*** 进入注册页面*/
const onToRegister = () => {// 配置跳转方式store.commit('app/changeRouterType', 'push')router.push('/register')
}
</script><style lang="scss" scoped></style>
02: 表单校验实现原理与方案分析
在绝大多数的情况下,我们进行登录时,都会通过 UI 组件库 实现表单校验功能。但是在没有 UI 组件库 的情况下,我们应该如何进行表单校验呢?
想要搞明白这一点,我们首先就需要搞明白表单校验的 实现原理。
表单校验的实现原理
我们知道,所谓表单校验,指的是:
1. 在某一个时机下(失去焦点、内容变化)
2. 检查表单元素中的 value 是否符合某个条件(校验条件)
3. 如果不符合,则给出对应的提示
根据以上描述,我们所需要关注的,其实就是三点内容:
1. 监听表单元素的对应时机
2. 检查内容是否匹配校验条件
3. 根据检查结果,展示对应提示
自定义表单校验方案分析
根据以上原理描述,如果我们想要自定义一套表单校验的功能逻辑,是不是就比较简单了:
1. 创建对应的 field 输入框组件
2. 该组件中,包含两个元素:
1. input 输入框
2. span 表示错误提示
3. 监听 input 输入框的 blur 失去焦点 事件
4. 根据 input 的 value 判断是否满足一个或多个指定的条件(比如:是否为空)
5. 如果不满足,则展示 span 标签,表示错误提示消息
文章中的方案实现
根据以上描述,我们确实可以实现一个基础的表单校验。但是这样的表单校验组件,很难具有 普适 性,因为实际开发中,表单校验的场景多种多样。比如:国际化处理。
把它抽离成一个 通用组件 意义并不大。咱们在文章中,就不会专门去实现这样的一个组件。而是会采用一种更加普适的方式。
这种方式就是:vee-validate
vee-validate 是一个 vue 中专门做表单校验的库,该库更加具有 普适 性,也更加适合大家在实际开发中的使用。
03: 基于 vee-validate 实现普适的表单校验
- src/views/login-register
- - validate.js
// 关键代码import {Form as VeeForm,Field as VeeField,ErrorMessage as VeeErrorMessage
} from 'vee-validate'// 三个组件的使用
// src/views/login-register/validate.js/*** 用户名的表单校验*/
export const validateUsername = (value) => {if (!value) {return '用户名为必填的'}if (value.length < 3 || value.length > 12) {return '用户名应该在 3-12 位之间'}return true
}/*** 密码的表单校验*/
export const validatePassword = (value) => {if (!value) {return '密码为必填的'}if (value.length < 6 || value.length > 12) {return '密码应该在 6-12 位之间'}return true
}/*** 确认密码的表单校验*/
export const validateConfirmPassword = (value, password) => {if (value !== password[0]) {return '两次密码输入必须一致'}return true
}
04: 什么是人类行为验证?它的目的、实现原理、构建方案分别是什么?
当表单校验完成之后,接下来我们就来处理 人类行为验证 模块。
想要搞清楚 人类行为验证,就需要搞明白三点内容:
1. 什么是人类行为验证。
2. 它的目的是什么。
3. 它的实现原理是什么。
4. 我们应该如何在项目中使用它。
什么是人类行为验证
在我们日常使用的应用中,人类行为验证其实已经无处不在了。
比如大家应该都见过如下场景:
以上场景,均属于人类行为验证模块。
目的
为什么需要有这样的一个东西呢?这样的一个东西对用户而言是非常讨厌的一个操作。
想要搞明白这个问题,大家就需要先搞清楚现在的应用面临的一个问题。
假如在一个博客系统中,它会根据博客的访问量进行首页排名。假设有一个人,写了一段脚本代码,构建出巨量的 IP 来不断地访问一个指定的博客。这个博客就会被顶到非常靠前的访问位置中。
又假如:在某些投票或者砍价的应用中,也有人利用一段脚本代码,伪造出巨量的用户去进行投票或者砍价的行为,这样的投票或者砍价是不是也就失去了原本的意义。
针对以上这种场景,我们应该如何防止呢?如何能够判断出,当前进行“投票”的操作是 人 进行的,而不是 机器 进行的呢?
想要解决这个问题,就需要使用到 人类行为验证 了。
简单来说,人类行为验证的目的就是:明确当前的操作是人完成的,而非机器。
它的实现原理是什么?
想要完成这样的判断,并且让判断准确,其实是非常复杂的:
人机验证通过对用户的行为数据、设备特征与网络数据构建多维度数据分析,采用完整的可信前端安全方案,保证数据采集的真实性、有效性。 比如以下几个方面(包括但不仅限于):
1. 浏览器特征检查:所有浏览器都有差异,可以通过各种前端相关手段检查浏览器环境的真实性。
2. 鼠标事件(click、move、hover、leave)。
3. 页面窗口(size、scroll、坐标)。
4. cookie。
通过收集到的多维度数据,分析并建立人类行为模型,以此来判断用户是否是一个机器人。
以这样的滚动为例:
人进行的拖动拼图和机器进行的拖动拼图,两者的 鼠标行为轨迹 是不同的。这个不同就是区分人和机器的关键。
我们如何在项目中使用它
目前人类行为验证的实现方案,主要分为两种:
1. 收费平台,年费在几万到几十万不等,有专门的技术人员帮助对接:
极验
网易易盾
2. 免费开源,验证的精准度需要看服务端的能力:
gitee 开源的 SliderCaptcha
我们这里主要是使用这个开源的 SliderCaptcha 实现。
大家在实际项目中可以根据实际情况进行处理。
05: 构建人类行为验证模块
- src/views/login-register/login
- - slider-captcha.vue
// src/views/login-register/login/slider-captcha.vue<template><divclass="fixed top-[20%] left-[50%] translate-x-[-50%] w-[340px] h-[270px] text-sm bg-white dark:bg-zinc-800 rounded border border-zinc-200 dark:border-zinc-900 shadow-3xl"><div class="flex items-center h-5 text-left px-1 mb-1"><span class="flex-grow dark:text-zinc-200">请完成安全验证</span><m-svg-iconname="refresh"fillClass="fill-zinc-900 dark:fill-zinc-200"class="w-3 h-3 p-0.5 rounded-sm duration-300 cursor-pointer hover:bg-zinc-200 dark:hover:bg-zinc-900"@click="onReset"></m-svg-icon><m-svg-iconname="close"fillClass="fill-zinc-900 dark:fill-zinc-200"class="ml-2 w-3 h-3 p-0.5 rounded-sm duration-300 cursor-pointer hover:bg-zinc-200 dark:hover:bg-zinc-900"@click="onClose"></m-svg-icon></div><div id="captcha"></div></div>
</template><script>
const EMITS_CLOSE = 'close'
const EMITS_SUCCESS = 'success'
</script><script setup>
import '@/vendor/SliderCaptcha/slidercaptcha.min.css'
import '@/vendor/SliderCaptcha/longbow.slidercaptcha.min.js'
import { getCaptcha } from '@/api/sys'
import { onMounted } from 'vue'const emits = defineEmits([EMITS_CLOSE, EMITS_SUCCESS])let captcha = null
onMounted(() => {captcha = sliderCaptcha({// 渲染位置id: 'captcha',// 用户拼图成功之后的回调async onSuccess(arr) {const res = await getCaptcha({behavior: arr})if (res) {emits(EMITS_SUCCESS)}},// 用户拼图失败之后的回调onFail() {console.log('onFail')},// 默认的验证方法,咱们不在此处进行验证,而是选择在用户拼图成功之后进行验证,所以此处永远返回为 trueverify() {return true}})
})/*** 重置*/
const onReset = () => {captcha.reset()
}/*** 关闭*/
const onClose = () => {emits(EMITS_CLOSE)
}
</script>
// index.html<!-- iconfont 在线图标,主要用于 sliderCaptcha --><link rel="stylesheet"href="https://at.alicdn.com/t/font_3042963_nv614canpao.css?spm=a313x.7781069.1998910419.47&file=font_3042963_nv614canpao.css" />
使用:
// src/views/login-register/login/index.vue<template><!-- 人类行为验证模块 --><slider-captcha-vuev-if="isSliderCaptchaVisible"@close="isSliderCaptchaVisible = false"@success="onCaptchaSuccess"></slider-captcha-vue>
</template>
<script setup>
import sliderCaptchaVue from './slider-captcha.vue'// 控制 sliderCaptcha 展示
const isSliderCaptchaVisible = ref(false)/*** 登录触发*/
const onLoginHandler = () => {isSliderCaptchaVisible.value = true
}/*** 人类行为验证通过*/
const onCaptchaSuccess = async () => {isSliderCaptchaVisible.value = false// 登录操作onLogin()
}
</script>
06: 用户登录行为处理
// src/views/login-register/login/index.vueconst store = useStore()
const router = useRouter()// 登录时的 loading
const loading = ref(false)
// 用户输入的用户名和密码
const loginForm = ref({username: '',password: ''
})
/*** 用户登录行为*/
const onLogin = async () => {loading.value = true// 执行登录操作try {await store.dispatch('user/login', {...loginForm.value,loginType: LOGIN_TYPE_USERNAME})} finally {loading.value = false}router.push('/')
}
我们希望把所有登录逻辑都放入 vuex 中。这是一种比较常见的封装方式。token 的处理、用户信息的处理、退出登录的处理、刷新 token,都可以在一块完成。
- src/store/modules
- - user.js
// src/store/modules/user.jsimport { loginUser, getProfile, registerUser } from '@/api/sys'
import md5 from 'md5'
import { message } from '@/libs'
import { LOGIN_TYPE_OAUTH_NO_REGISTER_CODE } from '@/constants'export default {namespaced: true,state: () => ({// 登录之后的 tokentoken: '',// 获取用户信息userInfo: {}}),mutations: {/*** 保存 token*/setToken(state, newToken) {state.token = newToken},/*** 保存用户信息*/setUserInfo(state, newInfo) {state.userInfo = newInfo}},actions: {/*** 注册*/async register(context, payload) {const { password } = payload// 注册return await registerUser({...payload,password: password ? md5(password) : ''})},/*** 登录*/async login(context, payload) {const { password } = payloadconst data = await loginUser({...payload,password: password ? md5(password) : ''})// QQ 扫码登录,用户未注册if (data.code === LOGIN_TYPE_OAUTH_NO_REGISTER_CODE) {return data.code}context.commit('setToken', data.token)context.dispatch('profile')},/*** 获取用户信息*/async profile(context) {const data = await getProfile()context.commit('setUserInfo', data)// 欢迎message('success',`欢迎您 ${data.vipLevel? '尊贵的 VIP' + data.vipLevel + ' 用户 ' + data.nickname: data.nickname} `,6000)},/*** 退出登录*/logout(context) {context.commit('setToken', '')context.commit('setUserInfo', {})// 退出登录之后,重新刷新下页面,// 因为对于前台项目而言,用户是否登录(是否为 VIP)看到的数据可能不同location.reload()}}
}
// 注意:在 src/store/index.js 中进行注册
npm i md5
07: 用户信息获取行为
企业级项目中常见的传递 token 方式:在 axios 请求头中
// src/utils/request.jsconst service = axios.create({baseURL: import.meta.env.VITE_BASE_API,timeout: 5000
})// 请求拦截器
service.interceptors.request.use((config) => {// config.headers.icode = '你需要在这里填入你的 icode'if (store.getters.token) {// 如果token存在 注入tokenconfig.headers.Authorization = `Bearer ${store.getters.token}`}return config // 必须返回配置},(error) => {return Promise.reject(error)}
)
// 代码在上一小节 src/store/modules/user.js 中。
08: 退出登录操作
// 代码在上一小节 src/store/modules/user.js 中。
// src/views/layout/components/header/header-my.vue<script setup>
import { confirm } from '@/libs'/*** menu Item 点击事件,也可以根据其他的 key 作为判定,比如 name*/
const onItemClick = (path) => {// 有路径则进行路径跳转if (path) {// 配置跳转方式store.commit('app/changeRouterType', 'push')router.push(path)return}// 无路径则为退出登录confirm('您确定要退出登录吗?').then(() => {// 退出登录不存在跳转路径store.dispatch('user/logout')})
}
</script>
09: token 超时处理
通常情况下 token 均具备时效性。在本文章中,token 失效后,服务端会返回 401.
当服务端返回 401 时,表示 token 超时,则需要重新登录。
对应的操作可以在 axios 的响应式拦截器中进行。
// src/utils/request.jsimport axios from 'axios'
import store from '@/store'
import { message as $message } from '@/libs'const service = axios.create({baseURL: import.meta.env.VITE_BASE_API,timeout: 5000
})// 请求拦截器
service.interceptors.request.use((config) => {// config.headers.icode = '你需要在这里填入你的 icode'if (store.getters.token) {// 如果token存在 注入tokenconfig.headers.Authorization = `Bearer ${store.getters.token}`}return config // 必须返回配置},(error) => {return Promise.reject(error)}
)// 响应拦截器
service.interceptors.response.use((response) => {const { success, message, data } = response.data// 要根据success的成功与否决定下面的操作if (success) {return data} else {$message('warn', message)// TODO:业务错误return Promise.reject(new Error(message))}},// code 非 200 时,回调函数。(error) => {// 处理 token 超时问题if (error.response &&error.response.data &&error.response.data.code === 401) {// TODO: token超时store.dispatch('user/logout')}$message('error', error.response.data.message)// TODO: 提示错误消息return Promise.reject(error)}
)export default service
10: 注册页面基本样式处理
- src/views/login-register
- - register
- - - index.vue
<template><divclass="relative h-screen bg-white dark:bg-zinc-800 text-center xl:bg-zinc-200"><!-- 头部图标 --><header-vue></header-vue><!-- 表单区 --><divclass="block px-3 mt-4 dark:bg-zinc-800 xl:bg-white xl:w-[388px] xl:dark:bg-zinc-900 xl:m-auto xl:mt-8 xl:py-4 xl:rounded-sm xl:shadow-lg"><h3class="mb-2 font-semibold text-base text-main dark:text-zinc-300 hidden xl:block">注册账号</h3><!-- 表单 --><vee-form @submit="onRegister"><!-- 用户名 --><vee-fieldclass="dark:bg-zinc-800 dark:text-zinc-400 border-b-zinc-400 border-b-[1px] w-full outline-0 pb-1 px-1 text-base focus:border-b-main dark:focus:border-b-zinc-200 xl:dark:bg-zinc-900"name="username"type="text"placeholder="用户名"autocomplete="on":rules="validateUsername"v-model="regForm.username"/><vee-error-messageclass="text-sm text-red-600 block mt-0.5 text-left"name="username"></vee-error-message><!-- 密码 --><vee-fieldclass="dark:bg-zinc-800 dark:text-zinc-400 border-b-zinc-400 border-b-[1px] w-full outline-0 pb-1 px-1 text-base focus:border-b-main dark:focus:border-b-zinc-200 xl:dark:bg-zinc-900"name="password"type="password"placeholder="密码"autocomplete="on":rules="validatePassword"v-model="regForm.password"/><vee-error-messageclass="text-sm text-red-600 block mt-0.5 text-left"name="password"></vee-error-message><!-- 确认密码 --><vee-fieldclass="dark:bg-zinc-800 dark:text-zinc-400 border-b-zinc-400 border-b-[1px] w-full outline-0 pb-1 px-1 text-base focus:border-b-main dark:focus:border-b-zinc-200 xl:dark:bg-zinc-900"name="confirmPassword"type="password"placeholder="确认密码"autocomplete="on"rules="validateConfirmPassword:@password"v-model="regForm.confirmPassword"/><vee-error-messageclass="text-sm text-red-600 block mt-0.5 text-left"name="confirmPassword"></vee-error-message><div class="pt-1 pb-3 leading-[0px] text-right"><div class="mb-2"><aclass="inline-block p-1 text-zinc-400 text-right dark:text-zinc-600 hover:text-zinc-600 dark:hover:text-zinc-400 text-sm duration-400 cursor-pointer"target="__black"@click="onToLogin">去登录</a></div><div class="text-center"><aclass="text-zinc-400 dark:text-zinc-600 hover:text-zinc-600 dark:hover:text-zinc-400 text-sm duration-400"href="https://m.imooc.com/newfaq?id=89"target="__black">注册即同意《慕课网注册协议》</a></div></div><m-buttonclass="w-full dark:bg-zinc-900 xl:dark:bg-zinc-800":isActiveAnim="false":loading="loading">立即注册</m-button></vee-form></div></div>
</template><script setup>
import headerVue from '../components/header.vue'
import {Form as VeeForm,Field as VeeField,ErrorMessage as VeeErrorMessage,defineRule
} from 'vee-validate'
import {validateUsername,validatePassword,validateConfirmPassword
} from '../validate'
import { LOGIN_TYPE_USERNAME } from '@/constants'
import { ref } from 'vue'
import { useStore } from 'vuex'
import { useRouter, useRoute } from 'vue-router'const store = useStore()
const router = useRouter()
const route = useRoute()/*** 插入规则*/
defineRule('validateConfirmPassword', validateConfirmPassword)/*** 进入登录页面*/
const onToLogin = () => {// 配置跳转方式store.commit('app/changeRouterType', 'push')router.push('/login')
}// 数据源
const regForm = ref({username: '',password: '',confirmPassword: ''
})
// loading
const loading = ref(false)
console.log(route)
/*** 触发注册*/
const onRegister = async () => {loading.value = truetry {const payload = {username: regForm.value.username,password: regForm.value.password}// 触发注册,携带第三方数据await store.dispatch('user/register', {...payload,...route.query})// 注册成功,触发登录await store.dispatch('user/login', {...payload,loginType: LOGIN_TYPE_USERNAME})} finally {loading.value = false}router.push('/')
}
</script><style lang="scss" scoped></style>
确认密码 要关联到 密码,这样的关联操作 需要进行一个单独的注册。
// src/views/login-register/validate.js/*** 确认密码的表单校验*/
export const validateConfirmPassword = (value, password) => {if (value !== password[0]) {return '两次密码输入必须一致'}return true
}// register/index.vue 中使用代码import { defineRule } from 'vee-validate'
import { validateConfirmPassword } from '../validate'/*** 插入规则*/
defineRule('validateConfirmPassword', validateConfirmPassword)<vee-field placeholder="确认密码"autocomplete="on"rules="validateConfirmPassword:@password"
/>
11: 处理注册行为
// src/store/modules/user.jsexport default {……actions: {……/*** 注册*/async register(context, payload) {const { password } = payload// 注册return await registerUser({...payload,password: password ? md5(password) : ''})},}
}
// src/views/login-register/register/index.vue
// 代码在上一小节中
12: 总结
在本篇文章中,我们主要处理了两块内容:
1. 人类行为验证
1. 是什么
2. 目的
3. 实现原理
4. 构建方案
2. 表单验证原理 以及在实际开发中 通过 vee-validate 实现表单验证功能
登录处理完成之后,接下来我们就需要处理用户的信息展示和修改了。
在用户信息展示和修改中,我们将接触到新的通用组件和图片裁剪、上传的概念。