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

❤️砥砺前行,不负余光,永远在路上❤️
❤️砥砺前行,不负余光,永远在路上❤️


在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

实现思路

有帮助的话各位哥哥可以点个关注收藏哦

后端生成六位随机验证码,存入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.js
const nodemailer = require('nodemailer');
const { mailConfig } = require('../config/index')
const { user, pass } = mailConfig

let 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 ports
	auth: {
		user,
		pass
	}
});
//pass 不是邮箱账户的密码而是stmp的授权码(必须是相应邮箱的stmp授权码)
//邮箱---设置--账户--POP3/SMTP服务---开启---获取stmp授权码

module.exports = function (email, code) {
	//     const {username,password,email} = user
	let mailOptions = {
		from: '<nmxgzs@foxmail.com>', // 发送方
		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.body
	client.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>


总结

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

  • 8
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

codernmx

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值