nodejs+express实现用户登录或者注册通过邮箱发送验证码(redis验证)
❤️砥砺前行,不负余光,永远在路上❤️
❤️砥砺前行,不负余光,永远在路上❤️
简要目录
- 实现思路
- 一、后端部分(文件目录可以看图2)
-
- 1.redis部分
- 2.nodemailer部分
- 3.发送邮件的接口
- 4.后端校验验证码是否有效
- 二、前端部分(使用的element-admin)
-
- 1.正则验证输入的是否是邮箱号
- 2.前端login页面完整代码可以参考(有部分字段需要修改),这个包括60秒倒计时的效果。
- 总结
实现思路
有帮助的话各位哥哥可以点个关注收藏哦
后端生成六位随机验证码,存入redis(key:邮箱号,value:验证码),校验的接口/code/login
通过redis 查询code是否存在,如果满足条件,可以自己加一些登录注册的业务之后在返回需要的值。
有帮助的话各位哥哥可以点个关注收藏哦
一、后端部分(文件目录可以看图2)
1.redis部分
代码如下(示例):
const redis = require('redis');const client = redis.createClient(); //默认没有密码 127.0.0.1 端口也是默认// 如果是连接远程的话// redis[s]://[[username][:password]@][host][:port][/db-number]:// const client = createClient({// url: 'redis://alice:foobared@awesome.redis.server:6380'// });client.on('error', (err) =>console.log('Redis Client Error', err));client.on('connect', () => {console.log('redis connect success');})client.connect();module.exports = client;
2.nodemailer部分
代码如下(示例):
//nodemailer.jsconst nodemailer = require('nodemailer');const { mailConfig } = require('../config/index')const { user, pass } = mailConfiglet transporter = nodemailer.createTransport({//node_modules/nodemailer/lib/well-known/services.json 查看相关的配置,如果使用qq邮箱,就查看qq邮箱的相关配置service: 'qq', //类型qq邮箱port: 465,secure: true, // true for 465, false for other portsauth: {user,pass}});//pass 不是邮箱账户的密码而是stmp的授权码(必须是相应邮箱的stmp授权码)//邮箱---设置--账户--POP3/SMTP服务---开启---获取stmp授权码module.exports = function (email, code) {// const {username,password,email} = userlet mailOptions = {from: '', // 发送方to: email, //接收者邮箱,多个邮箱用逗号间隔subject: `欢迎登录,你的验证码${code}`, // 标题html: `<head><base target="_blank" /><style type="text/css">::-webkit-scrollbar{ display: none; }</style><style id="cloudAttachStyle" type="text/css">#divNeteaseBigAttach, #divNeteaseBigAttach_bak{display:none;}</style><style id="blockquoteStyle" type="text/css">blockquote{display:none;}</style><style type="text/css"> body{font-size:14px;font-family:arial,verdana,sans-serif;line-height:1.666;padding:0;margin:0;overflow:auto;white-space:normal;word-wrap:break-word;min-height:100px} td, input, button, select, body{font-family:Helvetica, \'Microsoft Yahei\', verdana} pre {white-space:pre-wrap;white-space:-moz-pre-wrap;white-space:-pre-wrap;white-space:-o-pre-wrap;word-wrap:break-word;width:95%} th,td{font-family:arial,verdana,sans-serif;line-height:1.666} img{ border:0} header,footer,section,aside,article,nav,hgroup,figure,figcaption{display:block} blockquote{margin-right:0px}</style></head><body tabindex="0" role="listitem"><table width="700" border="0" align="center" cellspacing="0" style="width:700px;"><tbody><tr><td><div style="width:700px;margin:0 auto;border-bottom:1px solid #ccc;margin-bottom:30px;"><table border="0" cellpadding="0" cellspacing="0" width="700" height="39" style="font:12px Tahoma, Arial, 宋体;"><tbody><tr><td width="210"></td></tr></tbody></table></div><div style="width:680px;padding:0 10px;margin:0 auto;"><div style="line-height:1.5;font-size:14px;margin-bottom:25px;color:#4d4d4d;"><strong style="display:block;margin-bottom:15px;">尊敬的用户:<span style="color:#f60;font-size: 16px;"></span>您好!</strong><strong style="display:block;margin-bottom:15px;">您正在进行<span style="color: red">用户登录</span>操作,请在验证码输入框中输入:<span style="color:#f60;font-size: 24px">${code}</span>,以完成操作。</strong></div> <div style="margin-bottom:30px;"><small style="display:block;margin-bottom:20px;font-size:12px;"><p style="color:#747474;"> 注意:此操作可能会修改您的密码、登录邮箱或绑定手机。如非本人操作,请及时登录并修改密码以保证帐户安全<br>(工作人员不会向你索取此验证码,请勿泄漏!)</p></small></div></div><div style="width:700px;margin:0 auto;"><div style="padding:10px 10px 0;border-top:1px solid #ccc;color:#747474;margin-bottom:20px;line-height:1.3em;font-size:12px;"><p>此为系统邮件,请勿回复<br>请保管好您的邮箱,避免账号被他人盗用</p><p>网络科技团队</p></div></div></td></tr></tbody></table></body>`};transporter.sendMail(mailOptions, (error, info) => {if (error) {return console.log(error);}console.log('mail sent:', info.response);});};
3.发送邮件的接口
router中引入redis 和 nodemailer 部分
const client = require('../utils/redis');//redis使用const nodemailer = require('../utils/nodemailer');//发送邮件
//成功返回参数function success (res, total = null) {if (total) {return {code: 200,data: res,msg: '成功',total}} else {return {code: 200,data: res,msg: '成功'}}}//失败参数function fail (msg) {return {code: 500,msg}}// 生成六位随机验证码function createCode () {return parseInt(Math.random() * 1000000)// return 'xxxxxx'.replace(/[xy]/g, function (c) {// var r = (Math.random() * 16) | 0// var v = c == 'x' ? r : (r & 0x3) | 0x8// return v.toString(16)// })}//发送验证码邮件router.post('/send/email', function (req, response, next) {let code = createCode() //随机生成验证码const mail = req.body.mail//请求携带的邮件client.set(mail, code).then(res => { //存入redis//设置成功发送邮件nodemailer(mail, code)response.send(success())})client.expire(mail, 60);//设置过期时间 60s 前端六十秒可以重新获取});
4.后端校验验证码是否有效
//通过验证码登录router.post('/code/login', function (req, response, next) {/* 这里 用户名就是 邮件 密码就是code */const { mail, code} = req.bodyclient.get(mail).then(res => { //从redis查询数据if (code== res) {console.log('验证成功')//do something// ...response.send(success({user: mail,}))} else {console.log('验证失败')response.send(fail('验证失败'))}})});
二、前端部分(使用的element-admin)
1.正则验证输入的是否是邮箱号
const validateUsername = (rule, value, callback) => { let reg = /^[A-Za-z\d]+([-_.][A-Za-z\d]+)*@([A-Za-z\d]+[-.])+[A-Za-z\d]{2,5}$/; //!reg.test(value) if (!reg.test(value)) { callback(new Error('请输入正确邮箱号码')) } else { callback() } }
2.前端login页面完整代码可以参考(有部分字段需要修改),这个包括60秒倒计时的效果。
<template> <div class="login-container"> <el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form" autocomplete="on" label-position="left"> <div class="title-container"> <h3 class="title"> {{ $t('login.title') }} </h3> <!-- <lang-select class="set-language" /> --> </div> <el-form-item prop="username"> <el-row style="padding-right:5px"> <el-col :span="18"> <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-col> <el-col :span="6" style="margin-top:7px"> <el-button type="primary" :disabled="disable" :class="{ codeGeting:isGeting }" @click="getVerCode">{{getCode}}</el-button> </el-col> </el-row> </el-form-item> <el-tooltip v-model="capsTooltip" content="Caps lock is On" placement="right" manual> <el-form-item> <span class="svg-container"> <svg-icon icon-class="password" /> </span> <el-input :key="passwordType" ref="password" v-model="loginForm.password" 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-button :loading="loading" type="primary" style="width:100%;margin-bottom:30px;" @click.native.prevent="handleLogin"> {{ $t('login.logIn') }} </el-button> <div style="position:relative"> <div class="other-login"> <div class="title">推荐使用其他方式登录</div> <img src="@/assets/mp.png" class="wx-logo" title="小程序登录" alt="小程序登录" @click="otherLogin"> </div> </div> </el-form> <el-dialog title="微信扫码登录" :visible.sync="showDialog" align="center" width="30%" @close="wxLoginClose"> <div> <el-image :src="qrUrl" alt="小程序码" height="10%" /> <div style="margin:15px 0">请使用微信扫描小程序码登录{{ bindTimeout ? '(已超时)' : '' }}</div> <!-- (后期考虑是否启用选择性授权) --> <!-- <div> 启用授权获取用户信息: <el-switch v-model="auth" active-color="#13ce66" inactive-color="#ff4949" @change="authChange" /> </div> --> </div> </el-dialog> </div></template><script>import { validUsername } from '@/utils/validate'import { getCode, getToken, getUUid, sendMail, codeLogin } from '@/api/user'import { GlobalGetUuidShort } from '@/utils/index'export default { name: 'Login', components: {}, data () { const validateUsername = (rule, value, callback) => { let reg = /^[A-Za-z\d]+([-_.][A-Za-z\d]+)*@([A-Za-z\d]+[-.])+[A-Za-z\d]{2,5}$/; //!reg.test(value) if (!reg.test(value)) { callback(new Error('请输入正确邮箱号码')) } else { callback() } } const validatePassword = (rule, value, callback) => { if (value.length < 6) { callback(new Error('密码不能少于6位')) } else { callback() } } return { qrUrl: '', auth: true, bindTimeout: false, timer: null, // 定时器 loginForm: { username: '', password: '' }, loginRules: { username: [{ required: true, trigger: 'blur', validator: validateUsername }], }, passwordType: 'password', capsTooltip: false, loading: false, showDialog: false, redirect: undefined, otherQuery: {}, getCode: '获取验证码', isGeting: false, count: 60, disable: false } }, 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) }, 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: { //获取验证码 getVerCode () { if (this.loginForm.username) { sendMail(this.loginForm).then(res => { console.log(res, 'res') }) var countDown = setInterval(() => { if (this.count < 1) { this.isGeting = false this.disable = false this.getCode = '获取验证码' this.count = 60 clearInterval(countDown) } else { this.isGeting = true this.disable = true this.getCode = this.count-- + '秒后重发' } }, 1000) } else { this.$notify.error('请必须输入邮箱号码') } }, //关闭弹窗清除定时器 wxLoginClose () { this.timer && clearTimeout(this.timer) this.bindTimeout = false }, // 点击其他方式登录 otherLogin () { getToken().then(r => { this.showDialog = true this.getQrUrl() }) }, changeQr () { if (this.bindTimeout) { this.bindTimeout = false this.getQrUrl() } else { this.$notify.warning('请当前二维码过期之后重新获取') } }, getQrUrl () { let uuid = GlobalGetUuidShort(), counter = 1 this.qrUrl = `/api/getCode?useAuth=1&uuid=${uuid}` this.timer && clearTimeout(this.timer)// 清除定时器重新开启 this.timer = setInterval(() => { getUUid({ uuid }).then((res) => {// 获取openid counter++ if (counter === 31) { //超时 clearTimeout(this.timer) this.bindTimeout = true } if (res.data.openid !== '') { clearTimeout(this.timer) this.showDialog = false this.$store.dispatch('user/login', res.data).then(() => {// 登录跳转 (扫码登录)this.$router.push({ path: this.redirect || '/dashboard', query: this.otherQuery }) }).catch(err => {console.log(err, 'err') }) } }).catch((err) => { clearTimeout(this.timer) }) }, 2000) }, // 修改选项重新获取qr // authChange (val) { // console.log(val) // this.$nextTick(function () { // this.qrUrl = `/api/getCode?uuid=${this.uuid}` + '&useAuth=' + (val ? 1 : 0) // }) // }, 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() }) }, handleLogin () { this.$refs.loginForm.validate(valid => { if (valid) { this.loading = true // this.$message.warning('开发中,目前仅支持扫码登录') codeLogin(this.loginForm).then(res => { console.log(res, 'res') this.loading = false this.$store.dispatch('user/login', res.data).then(() => { console.log(55, '55') this.$router.push({ path: this.redirect || '/dashboard', query: this.otherQuery })}).catch(() => { // this.loading = false}) // this.$router.push({ path: this.redirect || '/dashboard', query: this.otherQuery }) }) // 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 }, {}) } // afterQRScan() { // if (e.key === 'x-admin-oauth-code') { // const code = getQueryObject(e.newValue) // const codeMap = { //wechat: 'code', //tencent: 'code' // } // const type = codeMap[this.auth_type] // const codeName = code[type] // if (codeName) { //this.$store.dispatch('LoginByThirdparty', codeName).then(() => { // this.$router.push({ path: this.redirect || '/' }) //}) // } else { //alert('第三方登录失败') // } // } // } }}</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 { .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; }}</style><style lang="scss" scoped>$bg: #2d3a4b;$dark_gray: #889aa4;$light_gray: #eee;.codeGeting { background: #cdcdcd; border-color: #cdcdcd;}.login-container { min-height: 100%; width: 100%; background-color: $bg; overflow: hidden; .mask { opacity: 0.2; } .login-form { position: relative; width: 520px; max-width: 100%; padding: 160px 35px 0; margin: 0 auto; overflow: hidden; .other-login { margin-top: 30px; text-align: center; .title { color: #dcdfe6; position: relative; font-size: 14px; &:before { position: absolute; left: 0; top: 50%; content: ''; width: 100px; height: 1px; background: #dcdfe6; display: inline-block; } &:after { position: absolute; right: 0; top: 50%; content: ''; width: 100px; height: 1px; background: #dcdfe6; display: inline-block; } } .wx-logo { margin-top: 20px; width: 36px; height: 36px; border-radius: 100%; cursor: pointer; } } } .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: 26px; color: $light_gray; margin: 0px auto 40px auto; text-align: center; font-weight: bold; } .set-language { color: #fff; position: absolute; top: 3px; font-size: 18px; right: 0px; cursor: pointer; } } .show-pwd { position: absolute; right: 10px; top: 7px; font-size: 16px; color: $dark_gray; cursor: pointer; user-select: none; } .thirdparty-button { position: absolute; right: 0; bottom: 6px; } @media only screen and (max-width: 470px) { .thirdparty-button { display: none; } }}</style>
总结
贴的代码应该都是完整的,如果哪里有问题的话可以留言哦