> 文档中心 > nodejs+express实现用户登录或者注册通过邮箱发送验证码(redis验证)

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>

总结

贴的代码应该都是完整的,如果哪里有问题的话可以留言哦

素彩网