网上的前端验证码逻辑总感觉不安全,验证码建议还是使用后端配合验证。
如果产品确定可以上网的话,就可以使用腾讯,百度等第三方验证,对接方便。但是产品可能内网部署,就必须自己写了。
本文章就是基于这一点来实现的。
前端验证码显示一个图片,后端生成图片。
1.前端调用生端获取图片时,传入一个roomID,后端生成一个4位验征码,放入redis中。然后生成一个图片返回。
2.前端显示图片,登录时将roomID和填写的验证码,一并提交,登录接口根据roomId从redis中取出验证码判断是否正确。
这样就相当于后端验证了。
大家觉得有问题什么,可以进行评论。谢谢。
前端部分代码
<template> <div class="login-container"> <vue-particles color="#ffffff" :particleOpacity="0.7" :particlesNumber="50" shapeType="circle" :particleSize="4" linesColor="#dedede" :linesWidth="1" :lineLinked="true" :lineOpacity="0.4" :linesDistance="150" :moveSpeed="2" :hoverEffect="true" hoverMode="grab" :clickEffect="true" clickMode="push" ></vue-particles> <el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form" autocomplete="on" label-position="left"> <div class="title-container"> <h3 class="title">智能综合管理系统</h3> </div> <el-form-item prop="username"> <span class="svg-container"> <svg-icon icon-class="user" /> </span> <el-input ref="username" v-model="loginForm.username" placeholder="用户名" name="username" type="text" tabindex="1" autocomplete="on" /> </el-form-item> <el-tooltip v-model="capsTooltip" content="Caps lock is On" placement="right" manual> <el-form-item prop="password"> <span class="svg-container"> <svg-icon icon-class="password" /> </span> <el-input :key="passwordType" ref="password" v-model="loginForm.password" :type="passwordType" placeholder="密码" name="password" tabindex="2" autocomplete="on" @keyup.native="checkCapslock" @blur="capsTooltip = false" @keyup.enter.native="handleLogin" /> <span class="show-pwd" @click="showPwd"> <svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" /> </span> </el-form-item> </el-tooltip> <el-form-item prop="yzm"> <span class="svg-container"> <svg-icon icon-class="password" /> </span> <el-input type="text" v-model="loginForm.verifyCode" maxlength="4" placeholder="验证码" /> <div class="identifyCode" @click="refreshCode"> <el-image :src="verifyImageUrl"></el-image> </div> </el-form-item> <el-button :loading="loading" type="primary" style="width: 100%; margin-bottom: 30px" @click.native.prevent="handleLogin">登录</el-button> </el-form> </div> </template> <script> import { validUsername } from '@/utils/validate' import Identify from './components/Identify' import { uuid } from 'vue-uuid'; // uuid object is also exported to things // outside Vue instance. export default { name: 'Login', components: { Identify }, data() { const validateUsername = (rule, value, callback) => { if (!validUsername(value)) { callback(new Error('请输入正确的用户名')) } else { callback() } } const validatePassword = (rule, value, callback) => { if (value.length < 6) { callback(new Error('密码最少6位')) } else { callback() } } return { loginForm: { username: 'admin', password: '111111', roomId: '', verifyCode: '' }, loginRules: { username: [{ required: true, trigger: 'blur', validator: validateUsername }], password: [{ required: true, trigger: 'blur', validator: validatePassword }] }, passwordType: 'password', capsTooltip: false, loading: false, showDialog: false, redirect: undefined, otherQuery: {}, identifyCodes: '1234567890', identifyCode: '', // verifyImageRoomId: '', verifyImageUrl: '', } }, watch: { $route: { handler: function (route) { const query = route.query if (query) { this.redirect = query.redirect this.otherQuery = this.getOtherQuery(query) } }, immediate: true } }, created() { // window.addEventListener('storage', this.afterQRScan) // this.identifyCode = '' // this.makeCode(this.identifyCodes, 4) this.refreshCode() }, mounted() { if (this.loginForm.username === '') { this.$refs.username.focus() } else if (this.loginForm.password === '') { this.$refs.password.focus() } }, destroyed() { // window.removeEventListener('storage', this.afterQRScan) }, methods: { checkCapslock(e) { const { key } = e this.capsTooltip = key && key.length === 1 && (key >= 'A' && key <= 'Z') }, showPwd() { if (this.passwordType === 'password') { this.passwordType = '' } else { this.passwordType = 'password' } this.$nextTick(() => { this.$refs.password.focus() }) }, createUuid() { let uuidV4 = uuid.v4().replace('-', '').replace('-', '').replace('-', '').replace('-', '') this.verifyImageRoomId = uuidV4 this.verifyImageUrl = '/api/Operator/getCaptchaImage/120/40/' + this.verifyImageRoomId console.log(uuidV4) }, handleLogin() { this.$refs.loginForm.validate(valid => { if (valid) { this.loading = true this.$store.dispatch('user/login', this.loginForm) .then(() => { this.$router.push({ path: this.redirect || '/', query: this.otherQuery }) this.loading = false }) .catch(() => { this.loading = false }) } else { console.log('error submit!!') return false } }) }, getOtherQuery(query) { return Object.keys(query).reduce((acc, cur) => { if (cur !== 'redirect') { acc[cur] = query[cur] } return acc }, {}) }, // 生成随机数 randomNum(min, max) { return Math.floor(Math.random() * (max - min) + min) }, // 切换验证码 refreshCode() { let uuidV4 = uuid.v4().replace('-', '').replace('-', '').replace('-', '').replace('-', '') this.loginForm.roomId = uuidV4 this.verifyImageUrl = '/api/Operator/getCaptchaImage/120/40/' + this.loginForm.roomId console.log(uuidV4) }, // 生成四位随机验证码 makeCode(o, l) { for (let i = 0; i < l; i++) { this.identifyCode += this.identifyCodes[ this.randomNum(0, this.identifyCodes.length) ] } console.log(this.identifyCode) } } } </script> <style lang="scss"> /* 修复input 背景不协调 和光标变色 */ /* Detail see https://github.com/PanJiaChen/vue-element-admin/pull/927 */ $bg: #283443; $light_gray: #fff; $cursor: #fff; @supports (-webkit-mask: none) and (not (cater-color: $cursor)) { .login-container .el-input input { color: $cursor; } } /* reset element-ui css */ .login-container { background: url("~@/assets/background.jpg") no-repeat; min-height: 100vh; .el-input { display: inline-block; height: 47px; width: 85%; input { background: transparent; border: 0px; -webkit-appearance: none; border-radius: 0px; padding: 12px 5px 12px 15px; color: $light_gray; height: 47px; caret-color: $cursor; &:-webkit-autofill { box-shadow: 0 0 0px 1000px $bg inset !important; -webkit-text-fill-color: $cursor !important; } } } .el-form-item { border: 1px solid rgba(255, 255, 255, 0.1); background: rgba(0, 0, 0, 0.1); border-radius: 5px; color: #454545; .el-form-item__error { color: rgb(223, 223, 176); } } } </style> <style lang="scss" scoped> $bg: #2d3a4b; $dark_gray: #889aa4; $light_gray: #eee; .login-container { min-height: 100%; width: 100%; background-color: $bg; overflow: hidden; .login-form { position: absolute; left: 0; right: 0; top: 0; bottom: 0; margin: auto; width: 520px; max-width: 100%; padding: 160px 35px 0; margin: 0 auto; overflow: hidden; } .tips { font-size: 14px; color: #fff; margin-bottom: 10px; span { &:first-of-type { margin-right: 16px; } } } .svg-container { padding: 6px 5px 6px 15px; color: $dark_gray; vertical-align: middle; width: 30px; display: inline-block; } .title-container { position: relative; .title { font-size: 42px; color: $light_gray; margin: 0px auto 40px auto; text-align: center; font-weight: bold; } } .show-pwd { position: absolute; right: 10px; top: 7px; font-size: 16px; color: $dark_gray; cursor: pointer; user-select: none; } .identifyCode { position: absolute; right: 10px; top: 5px; } .thirdparty-button { position: absolute; right: 0; bottom: 6px; } @media only screen and (max-width: 470px) { .thirdparty-button { display: none; } } } </style>
后端接口
/// <summary> /// 获取验证码 /// </summary> /// <returns></returns> [HttpGet("getCaptchaImage/{width:int}/{height:int}/{roomId}")] public IActionResult GetCaptchaImage(int width, int height, string roomId) { Console.WriteLine(roomId); //int width = 100; //int height = 36; var captchaCode = Captcha.GenerateCaptchaCode(); var result = Captcha.GenerateCaptchaImage(width, height, captchaCode); string redisKey = "VerifyCode_" + roomId; Startup.redisDb.StringSet(redisKey, captchaCode, new TimeSpan(0, 5, 0)); Stream s = new MemoryStream(result.CaptchaByteData); return new FileStreamResult(s, "image/png"); } /// <summary> /// 登录 /// </summary> /// <returns></returns> [HttpPost("login")] public ApiResponseData Login(LoginDto loginInfo) { if (string.IsNullOrWhiteSpace(loginInfo.username)) return ApiResponse.Error("用户名不能为空"); if (string.IsNullOrWhiteSpace(loginInfo.password)) return ApiResponse.Error("密码不能为空"); Entity.BaseOperator operInfo = Db .Select<BaseOperator>() .Where(a => a.OperatorCode == loginInfo.username && a.Password == loginInfo.password.ToLower() && a.Status == 1 && a.IsDel == false).ToOne(); if (operInfo == null) return ApiResponse.Error("用户名或者密码不正确"); bool verifyResult = Captcha.ValidateCaptchaCode(loginInfo.RoomId, loginInfo.VerifyCode); if(verifyResult == false) return ApiResponse.Error("验证码不正确"); //登录时重新生成token,一个用户只能在一个地方登录 string token = System.Guid.NewGuid().ToString().Replace("-", ""); Db.Update<BaseOperator>(operInfo.OperatorId) .Set(a => a.Token, token) .ExecuteAffrows(); dynamic outJson = new ExpandoObject();//初始化一个不包含任何成员的ExpandoObject outJson.token = token; //存入redis string redisKey = "UserInfo_" + token; Startup.redisDb.StringSet(redisKey, operInfo.OperatorId, new TimeSpan(24, 0, 0)); return ApiResponse.Succ(outJson); }