美文网首页Web 前端开发 让前端飞
Koa+MongoDB+smtp+passport实现登录注册邮

Koa+MongoDB+smtp+passport实现登录注册邮

作者: 超人鸭 | 来源:发表于2020-01-13 15:19 被阅读0次

    记录一个注册登录并有邮箱验证功能的功能,这个功能会用到很多插件,我把它拆分成几个步骤,可能需要看完全部步骤思路才会比较清晰,而且一些第三方插件的实现流程比较复杂,晦涩难懂,超人鸭弄了很多遍也只是停留在会用的阶段,但放心的是,在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

    相关文章

      网友评论

        本文标题:Koa+MongoDB+smtp+passport实现登录注册邮

        本文链接:https://www.haomeiwen.com/subject/hfogactx.html