Koa+MongoDB+smtp+passport实现登录注册邮箱验证流程

记录一个注册登录并有邮箱验证功能的功能,这个功能会用到很多插件,我把它拆分成几个步骤,可能需要看完全部步骤思路才会比较清晰,而且一些第三方插件的实现流程比较复杂,晦涩难懂,超人鸭弄了很多遍也只是停留在会用的阶段,但放心的是,在node中实现完整的登录注册功能,基本都是用这些插件,所以学会怎么用也是不错的。

首先看看前端的表单,一些校验直接在前端就可以完成:


image.png

接下来就是实现具体功能的步骤:

  1. 安装软件

1.MongoDB: 安装完设置环境变量,就可以通过git bash命令: mongod启动服务
2.redis: 安装完后可以通过git bash命令:redis-server启动

这两款软件最好装一下图形化界面,看着比较直观,redis是为了用来存储用户注册时邮箱验证码、过期时间、登录信息等,也可以直接用MongoDB来存储

  1. 邮箱设置
    用的是qq邮箱,没有原因,方便。
    打开qq邮箱点设置,点账户,开启前面两项后保存授权码:

image.png

这个授权码是唯一的,记得保密好

  1. 安装插件:
    1.mongoose   // 操作MongoDB
    2.koa-generic-session
    3.koa-redis  // 配合上个插件操作session
    4.koa-passport // 实现登录注册流程
    5.passport-local  // 本地策略,koa-passport就是对passport-local进行封装
    6.nodemailer  // 发送邮件的插件

一些基本的koa插件上面就没有列出来,像koa-router,koa-bodyparser

  1. 文件结构,这是我个人的文件结构


    image.png
  2. 编写配置文件:config.js
export default{
  // 数据库配置,users为数据库名称
  dbs:'mongodb://127.0.0.1:27017/users',
  // redis配置
  redis:{
    get host() {
      return '127.0.0.1'
    },
    get port() {
      return 6379
    }
  },
  // 邮箱服务配置
  smtp: {
    get host() { // 服务主机地址,为腾讯邮箱
      return 'smtp.qq.com'
    },
    get user() { // 发送者的邮箱
      return '1274085986@qq.com'
    },
    get pass() { // 发送者的邮箱凭证,记得保密
      return 'xxxxxxxxxxxxxxxxxx'
    },
    get code() { // 随机生成四位数的验证码
      return () => {
        return Math.random().toString(16).slice(2,6).toUpperCase()
      }
    },
    get expire() { // 过期时间,为调用时间加一分钟
      return () => {
        return new Date().getTime() + 60 * 1000
      }
    }
  },
}
  1. 编写MongoDB数据表结构,到dbs的models文件夹下面的users.js:
// 固定写法
import mongoose from 'mongoose'
const Schema = mongoose.Schema
const UserSchema = new Schema({
  username:{
    type:String,
    unique:true,
    require: true
  },
  password:{
    type:String,
    require: true
  },
  email:{
    type:String,
    require: true
  }
})
export default mongoose.model('User',UserSchema)
  1. 编写passport权限认证,possport.js:
import passport from 'koa-passport'
import LocalStrategy from 'passport-local' // 本地策略
import UserModel from '../../dbs/models/users'

// 在使用 passport.authenticate('策略', ...) 的时候,会执行策略
passport.use(new LocalStrategy(async function(username, password, done) {
  let where = {
    username
  }
  let result = await UserModel.findOne(where)
  if(result!=null) {
    if(result.password === password) {
      return done(null, result)
    } else {
      return done(null,false,'密码错误')
    }
  } else {
    return done(null, false, '用户不存在')
  }
}))

// 序列化ctx.login()触发
passport.serializeUser(function(user,done){
  done(null,user)
})

// 反序列化(请求时,session中存在"passport":{"user":"1"}触发)
passport.deserializeUser(function(user,done) {
  return done(null,user)
})

export default passport

整个流程我觉得最难懂的就是这块,passport-local插件是一个本地策略,koa-passport是对passport的一个封装,其中具体的流程逻辑还需要自己去研究。

  1. 在index.js中导入:
// 这只是上面说到的插件,一个koa项目可能要用到其他更多的插件
import mongoose from 'mongoose'
import bodyParser from 'koa-bodyparser'
import session from 'koa-generic-session'
import Redis from 'koa-redis'
import dbConfig from './dbs/config'
import passport from './interface/utils/passport'

app.keys = ['mt', 'keyskes'] // 设置cookie的签名
app.proxy = true
// key设置cookie的key,store设置外部存储
app.use(session({key:'mt',prefix: 'mt:uid', store: new Redis()}))
app.use(bodyParser({
    extendTypes: ['json','from','text']
}))

// 链接数据库
mongoose.connect(dbConfig.dbs,{
    useNewUrlParser: true,
    useUnifiedTopology: true
})

// 引入权限认证
/**
    * app.use(passport.initialize()) 会在请求周期ctx对象挂载以下方法与属性
    * ctx.state.user 认证用户
    * ctx.login(user) 登录用户(序列化用户)
    * ctx.isAuthenticated() 判断是否认证
*/
app.use(passport.initialize())
app.use(passport.session())
  1. 编写发送验证码的接口,到interface下的users.js:
import Router from 'koa-router'
import Redis from 'koa-redis'
import nodeMailer from 'nodemailer'
import User from '../dbs/models/users'
import Passport from './utils/passport'
import Email from '../dbs/config'  // 这里对配置的操作都是操作邮箱的

let router = new Router({  // 定义路由前缀
  prefix:'/users'
})

let Store = new Redis(Email.redis).client  // 初始化redis

router.post('/verify', async (ctx, next) => {
  let username = ctx.request.body.username
  /*
      *下面一发送就会已用户名为表名,以code验证码、expire该验证码的过期时间、email邮箱为字段存储
      *saveExpire 取得这个用户名发送的的验证码的过期时间
      *这一步是为了防止用户点了发送验证码然后在一分钟内再次点击
  */
  const saveExpire = await Store.hget(`nodemail:${username}`,'expire')
  // 防止用户频繁请求
  if(saveExpire && new Date().getTime() - saveExpire < 0) {
    ctx.body = {
      code: -1,
      msg: '验证请求过于频繁,1分钟1次'
    }
    return false
  }
  // 设置邮箱配置
  let transporter = nodeMailer.createTransport({
    host:Email.smtp.host, // 设置邮箱服务的主机,smtp.qq.com
    port:587, // 对应的端口号
    secure: false,
    auth: { // 用户信息
      user: Email.smtp.user,  // 发送者的邮箱
      pass: Email.smtp.pass  // 发送者邮箱的凭证,此处用qq邮箱
    }
  })
  // 一些配置信息
  let ko = {
    code: Email.smtp.code(), // 从配置文件中取
    expire: Email.smtp.expire(), // 从配置文件中取过期时间
    email: ctx.request.body.email,
    user: ctx.request.body.username
  }
  // 设置收件人信息
  let mailOptions = {
    from: `"认证邮件"<${Email.smtp.user}>`, // 设置发件人名称
    to: ko.email, // 发给谁
    subject:'超人鸭', // 主题
    html:`您的邀请码是${ko.code}` // html模板
  }
  // 发送邮件
  await transporter.sendMail(mailOptions,(err,info) => {
    if(err) {
      return console.log('error')
    } else { // 发送成功
      // 存储到redis
      Store.hmset(`nodemail:${ko.user}`, 'code', ko.code, 'expire', ko.expire, 'email', ko.email)
    }
  })
  ctx.body = {
    code: 0,
    msg: '验证码已发送,可能会有延时,有效期1分钟'
  }
})
  1. 注册接口
router.post('/signup', async (ctx) => {
  const {
    username,
    password,
    email,
    code
  } = ctx.request.body;
  if(code) {
    // 从redis中取出该用户对应的code和code过期时间
    const saveCode = await Store.hget(`nodemail:${username}`,'code')
    const saveExpire = await Store.hget(`nodemail:${username}`,'expire')
    if(code != saveCode) {
      ctx.body = {
        code: -1,
        msg: '请填写正确的验证码'
      }
      return
    } else { // 验证码相同
      if(new Date().getTime() - saveExpire > 0) {
        ctx.body = {
          code: -1,
          msg: '验证码已过期'
        }
        return
      } else { // 不会过期
        // 从数据库查询数据
        let user = await User.find({
          username
        })
        if(user.length) {
          ctx.body = {
            code: -1,
            msg: '已被注册'
          }
          return
        }
        let nuser = await User.create({ // 往数据库添加记录
          username,
          password,
          email
        })
        if(nuser) { // 注册成功
          ctx.body = {
            code: 0,
            msg: '注册成功'
          }
          return
        } else {
          ctx.body = {
            code: -1,
            msg: '注册失败'
          }
          return
        }
      }
    }
  } else {
    ctx.body = {
      code: -1,
      msg: '请填写验证码'
    }
  }
})
  1. 登录接口,就会用到上面说的passport来管理登录状态
router.post('/signin', async(ctx,next) => {
  /**
   * 使用koa-passport插件登录成功后可以设置ctx的状态,并且可以把用户信息存储在session中
   * 需要安装session中间件
   * 使用到本地策略,在passport.js已编写好逻辑
   */
  return Passport.authenticate('local',function(err,user,info,status) {
    if(err) {
      ctx.body = {
        code: -1,
        msg: err
      }
    } else {
      if(user) {
        ctx.body = {
          code: 0,
          msg: '登录成功',
          user
        }
        // passport封装的api,用来管理session
        return ctx.login(user)
      } else {
        ctx.body = {
          code: 1,
          msg: info
        }
      }
    }
  })(ctx, next)
})
  1. 退出登录和获取用户信息接口:
router.get('/exit', async(ctx, next) => {
  await ctx.logout() // passport封装的api,用来管理session
  // ctx.isAuthenticated() 判断是否认证
  if(!ctx.isAuthenticated()) {  // 说明退出成功
    ctx.body = {
      code: 0
    }
  } else {
    ctx.body = {
      code: -1
    }
  }
})

router.get('/getUser', async(ctx) => {
  if(ctx.isAuthenticated()) {
    const {username,email} = ctx.session.passport.user
    ctx.body = {
      user:username,
      email
    }
  } else {
    ctx.body = {
      user: '',
      email: ''
    }
  }
})
  1. 在index.js中引入该路由
    记得在users.js接口中导出路由:
export default router

在index.js中:

import users from './interface/users'
app.use(users.routes()).use(users.allowedMethods())

node的代码到这里就结束了,前端用的是vue,请求用的是axios,如果出现跨域,可以在前端代理,或者在koa中装一个koa2-cors插件,解决跨域的,用法也很简单,这里就不写了,还有前端的请求也很简单。

到这里整个流程就走完了,包括注册登录退出登录等,最复杂的passport超人鸭现在也说不太清,暂且处于能用的阶段,如果哪位朋友刚好学到这部分,请说出你的理解,欢迎指教哦。

作者微信:Promise_fulfilled

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容