基於 Vue + Koa2 + MongoDB + Redis 實現一個完整的登陸註冊

項目地址:https://github.com/caochangkui/vue-element-responsive-demo/tree/login-register

經過 vue-cli3.0 + Element 構建項目前端,Node.js + Koa2 + MongoDB + Redis 實現數據庫和接口設計,包括郵箱驗證碼、用戶註冊、用戶登陸、查看刪除用戶等功能。css

1. 技術棧

2. 項目依賴:

"dependencies": { 
    "axios": "^0.18.0",
    "crypto-js": "^3.1.9-1", 
    "element-ui": "^2.4.5",
    "js-cookie": "^2.2.0",
    "jsonwebtoken": "^8.5.0", 
    "koa": "^2.7.0",
    "koa-bodyparser": "^4.2.1",
    "koa-generic-session": "^2.0.1",
    "koa-json": "^2.0.2",
    "koa-redis": "^3.1.3",
    "koa-router": "^7.4.0",
    "mongoose": "^5.4.19",
    "nodemailer": "^5.1.1",
    "nodemon": "^1.18.10", 
    "vue": "^2.5.21", 
    "vue-router": "^3.0.1",
    "vuex": "^3.0.1"
  }

3. 前端實現步驟

3.1 登陸註冊頁面

經過 vue-cli3.0 + Element 構建項目前端頁面html

登陸頁(@/view/users/Login.vue):

註冊頁(@/view/users/Register.vue):

發送驗證碼前須要驗證用戶名和郵箱,用戶名必填,郵箱格式需正確。前端

用戶設置頁(@/view/users/setting/Setting.vue)vue

用戶登陸後,能夠進入用戶設置頁查看用戶和刪除用戶node

3.2 Vuex 狀態管理

經過 vuex 實現保存或刪除用戶 token,保存用戶名等功能。webpack

因爲使用單一狀態樹,應用的全部狀態會集中到一個比較大的對象。當應用變得很是複雜時,store 對象就有可能變得至關臃腫。ios

爲了解決以上問題,Vuex 容許咱們將 store 分割成模塊(module)。每一個模塊擁有本身的 state、mutation、action、getter。git

根目錄下新建store文件夾,建立modules/user.js:github

const user = {
  state: {
    token: localStorage.getItem('token'),
    username: localStorage.getItem('username')
  },

  mutations: {
    BIND_LOGIN: (state, data) => {
      localStorage.setItem('token', data)
      state.token = data
    },
    BIND_LOGOUT: (state) => {
      localStorage.removeItem('token')
      state.token = null
    },
    SAVE_USER: (state, data) => {
      localStorage.setItem('username', data)
      state.username = data
    }
  }
}

export default user

建立文件 getters.js 對數據進行處理輸出:web

const getters = {
    sidebar: state => state.app.sidebar,
    device: state => state.app.device,
    token: state => state.user.token,
    username: state => state.user.username
  }
export default getters

建立文件 index.js 管理全部狀態:

import Vue from 'vue'
import Vuex from 'vuex' 
import user from './modules/user'
import getters from './getters'

Vue.use(Vuex)

const store = new Vuex.Store({
  modules: { 
    user
  },
  getters
})

export default store

3.3 路由控制/攔截

路由配置(router.js):

import Vue from 'vue'
import Router from 'vue-router' 
const Login = () => import(/* webpackChunkName: "users" */ '@/views/users/Login.vue')
const Register = () => import(/* webpackChunkName: "users" */ '@/views/users/Register.vue')  
const Setting = () => import(/* webpackChunkName: "tables" */ '@/views/setting/Setting.vue') 

Vue.use(Router)

const router = new Router({ 
  base: process.env.BASE_URL,
  routes: [
    {
      path: '/login',
      name: 'Login',
      component: Login,
      meta: {
        title: '登陸'
      }
    },
    {
      path: '/register',
      name: 'Register',
      component: Register,
      meta: {
        title: '註冊'
      }
    },
    {
      path: '/setting',
      name: 'Setting',
      component: Setting,
      meta: {
        breadcrumb: '設置',
        requireLogin: true
      },
    }
  ]
})

路由攔截:

關於vue 路由攔截參考:http://www.javashuo.com/article/p-pzcvuewo-gy.html

// 頁面刷新時,從新賦值token
if (localStorage.getItem('token')) {
  store.commit('BIND_LOGIN', localStorage.getItem('token'))
}

// 全局導航鉤子
router.beforeEach((to, from, next) => {
  if (to.meta.title) { // 路由發生變化修改頁面title
    document.title = to.meta.title
  }
  if (to.meta.requireLogin) {
    if (store.getters.token) {
      if (Object.keys(from.query).length === 0) { // 判斷路由來源是否有query,處理不是目的跳轉的狀況
        next()
      } else {
          let redirect = from.query.redirect // 若是來源路由有query
          if (to.path === redirect) { // 避免 next 無限循環
              next()
          } else {
              next({ path: redirect }) // 跳轉到目的路由
          }
      }
    } else {
      next({
        path: '/login',
        query: { redirect: to.fullPath } // 將跳轉的路由path做爲參數,登陸成功後跳轉到該路由
      })
    }
  } else {
    next()
  }
})

export default router

3.4 Axios 封裝

封裝 Axios

// axios 配置
import axios from 'axios'
import store from './store'
import router from './router'

//建立 axios 實例
let instance = axios.create({
  timeout: 5000, // 請求超過5秒即超時返回錯誤
  headers: { 'Content-Type': 'application/json;charset=UTF-8' },
})

instance.interceptors.request.use(
  config => {
    if (store.getters.token) { // 若存在token,則每一個Http Header都加上token
      config.headers.Authorization = `token ${store.getters.token}`
      console.log('拿到token')
    }
    console.log('request請求配置', config)
    return config
  },
  err => {
    return Promise.reject(err)
  })

// http response 攔截器
instance.interceptors.response.use(
  response => {
    console.log('成功響應:', response)
    return response
  },
  error => {
    if (error.response) {
      switch (error.response.status) {
        case 401:
          // 返回 401 (未受權) 清除 token 並跳轉到登陸頁面
          store.commit('BIND_LOGOUT')
          router.replace({
            path: '/login',
            query: {
              redirect: router.currentRoute.fullPath
            }
          })
          break
        default:
          console.log('服務器出錯,請稍後重試!')
          alert('服務器出錯,請稍後重試!')
      }
    }
    return Promise.reject(error.response) // 返回接口返回的錯誤信息
  }
)

export default {
  // 發送驗證碼
  userVerify (data) {
    return instance.post('/api/verify', data)
  },
  // 註冊
  userRegister (data) {
    return instance.post('/api/register', data)
  },
  // 登陸
  userLogin (data) {
    return instance.post('/api/login', data)
  },
  // 獲取用戶列表
  getAllUser () {
    return instance.get('/api/alluser')
  },
  // 刪除用戶
  delUser (data) {
    return instance.post('/api/deluser', data)
  }
}

4. 服務端和數據庫實現

在根目錄下建立 server 文件夾,存放服務端和數據庫相關代碼。

4.1 MongoDB和Redis

建立 /server/dbs/config.js ,進行數據庫和郵箱配置

// mongo 鏈接地址
const dbs = 'mongodb://127.0.0.1:27017/[數據庫名稱]'

// redis 地址和端口
const redis = {
  get host() {  
    return '127.0.0.1'
  },
  get port() {
    return 6379
  }
}

// qq郵箱配置
const smtp = {
  get host() {
    return 'smtp.qq.com'
  },
  get user() {
    return '1********@qq.com' // qq郵箱名
  },
  get pass() {
    return '*****************' // qq郵箱受權碼
  },
  // 生成郵箱驗證碼
  get code() {
    return () => {
      return Math.random()
        .toString(16)
        .slice(2, 6)
        .toUpperCase()
    }
  },
  // 定義驗證碼過時時間rules,5分鐘
  get expire() {
    return () => {
      return new Date().getTime() + 5 * 60 * 1000
    }
  }
}

module.exports = {
  dbs,
  redis,
  smtp
}

使用 qq 郵箱發送驗證碼,須要在「設置/帳戶」中打開POP3/SMTP服務和MAP/SMTP服務。

4.2 Mongo 模型

建立 /server/dbs/models/users.js:

// users模型,包括四個字段
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const UserSchema = new Schema({
  username: {
    type: String,
    unique: true,
    required: true
  },
  password: {
    type: String,
    required: true
  },
  email: {
    type: String,
    required: true
  },
  token: {
    type: String,
    required: true
  }
})

module.exports = {
  Users: mongoose.model('User', UserSchema)
}

4.3 接口實現

建立 /server/interface/user.js:

const Router = require('koa-router')
const Redis = require('koa-redis') // key-value存儲系統, 存儲用戶名,驗證每一個用戶名對應的驗證碼是否正確
const nodeMailer = require('nodemailer') // 經過node發送郵件
const User = require('../dbs/models/users').Users
const Email = require('../dbs/config')

// 建立和驗證token, 參考4.4
const createToken = require('../token/createToken.js') // 建立token
const checkToken = require('../token/checkToken.js') // 驗證token


// 建立路由對象
const router = new Router({
  prefix: '/api' // 接口的統一前綴
})

// 獲取redis的客戶端
const Store = new Redis().client

// 接口 - 測試
router.get('/test', async ctx => {
  ctx.body = {
    code: 0,
    msg: '測試',
  }
})

// 發送驗證碼 的接口
router.post('/verify', async (ctx, next) => {
  const username = ctx.request.body.username
  const saveExpire = await Store.hget(`nodemail:${username}`, 'expire') // 拿到過時時間

  console.log(ctx.request.body)
  console.log('當前時間:', new Date().getTime())
  console.log('過時時間:', saveExpire)

  // 檢驗已存在的驗證碼是否過時,以限制用戶頻繁發送驗證碼
  if (saveExpire && new Date().getTime() - saveExpire < 0) {
    ctx.body = {
      code: -1,
      msg: '發送過於頻繁,請稍後再試'
    }
    return
  }

  // QQ郵箱smtp服務權限校驗
  const transporter = nodeMailer.createTransport({
    /**
     *  端口465和587用於電子郵件客戶端到電子郵件服務器通訊 - 發送電子郵件。
     *  端口465用於smtps SSL加密在任何SMTP級別通訊以前自動啓動。
     *  端口587用於msa
     */
    host: Email.smtp.host,
    port: 587,
    secure: false, // 爲true時監聽465端口,爲false時監聽其餘端口
    auth: {
      user: Email.smtp.user,
      pass: Email.smtp.pass
    }
  })

  // 郵箱須要接收的信息
  const ko = {
    code: Email.smtp.code(),
    expire: Email.smtp.expire(),
    email: ctx.request.body.email,
    user: ctx.request.body.username
  }

  // 郵件中須要顯示的內容
  const mailOptions = {
    from: `"認證郵件" <${Email.smtp.user}>`, // 郵件來自
    to: ko.email, // 郵件發往
    subject: '邀請碼', // 郵件主題 標題
    html: `您正在註冊****,您的邀請碼是${ko.code}` // 郵件內容
  }

  // 執行發送郵件
  await transporter.sendMail(mailOptions, (err, info) => {
    if (err) {
      return console.log('error')
    } else {
      Store.hmset(`nodemail:${ko.user}`, 'code', ko.code, 'expire', ko.expire, 'email', ko.email)
    }
  })

  ctx.body = {
    code: 0,
    msg: '驗證碼已發送,請注意查收,可能會有延時,有效期5分鐘'
  }
})

// 接口 - 註冊
router.post('/register', async ctx => {
  const { username, password, email, code } = ctx.request.body

  // 驗證驗證碼
  if (code) {
    const saveCode = await Store.hget(`nodemail:${username}`, 'code') // 拿到已存儲的真實的驗證碼
    const saveExpire = await Store.hget(`nodemail:${username}`, 'expire') // 過時時間

    console.log(ctx.request.body)
    console.log('redis中保存的驗證碼:', saveCode)
    console.log('當前時間:', new Date().getTime())
    console.log('過時時間:', saveExpire)

    // 用戶提交的驗證碼是否等於已存的驗證碼
    if (code === saveCode) {
      if (new Date().getTime() - saveExpire > 0) {
        ctx.body = {
          code: -1,
          msg: '驗證碼已過時,請從新申請'
        }
        return
      }
    } else {
      ctx.body = {
        code: -1,
        msg: '請填寫正確的驗證碼'
      }
      return
    }
  } else {
    ctx.body = {
      code: -1,
      msg: '請填寫驗證碼'
    }
    return
  }

  // 用戶名是否已經被註冊
  const user = await User.find({ username })
  if (user.length) {
    ctx.body = {
      code: -1,
      msg: '該用戶名已被註冊'
    }
    return
  }
  // 若是用戶名未被註冊,則寫入數據庫
  const newUser = await User.create({
    username,
    password,
    email,
    token: createToken(this.username) // 生成一個token 存入數據庫
  })

  // 若是用戶名被成功寫入數據庫,則返回註冊成功
  if (newUser) {
    ctx.body = {
      code: 0,
      msg: '註冊成功',
    }
  } else {
    ctx.body = {
      code: -1,
      msg: '註冊失敗'
    }
  }
})


// 接口 - 登陸
router.post('/login', async (ctx, next) => {
  const { username, password } = ctx.request.body

  let doc = await User.findOne({ username })
  if (!doc) { 
    ctx.body = {
      code: -1,
      msg: '用戶名不存在'
    }
  } else if (doc.password !== password) {
    ctx.body = {
      code: -1,
      msg: '密碼錯誤'
    }
  } else if (doc.password === password) {
    console.log('密碼正確')
    let token = createToken(username) // 生成token 
    doc.token = token // 更新mongo中對應用戶名的token
    try {
      await doc.save() // 更新mongo中對應用戶名的token
      ctx.body = {
        code: 0,
        msg: '登陸成功',
        username,
        token
      }
    } catch (err) {
      ctx.body = {
        code: -1,
        msg: '登陸失敗,請從新登陸'
      }
    }
  }
})

// 接口 - 獲取全部用戶 須要驗證 token
router.get('/alluser', checkToken, async (ctx, next) => {
  try {
    let result = []
    let doc = await User.find({}) 
    doc.map((val, index) => {
      result.push({
        email: val.email,
        username: val.username,
      })
    }) 
    ctx.body = {
      code: 0,
      msg: '查找成功',
      result
    }
  } catch (err) {
    ctx.body = {
      code: -1,
      msg: '查找失敗',
      result: err
    }
  }
})

// 接口 - 刪除用戶 須要驗證 token
router.post('/deluser', checkToken, async (ctx, next) => {
  const { username } = ctx.request.body

  try {
    await User.findOneAndRemove({username: username})
    ctx.body = {
      code: 0,
      msg: '刪除成功',
    }
  } catch (err) {
    ctx.body = {
      code: -1,
      msg: '刪除失敗',
    }
  }
})

module.exports = {
  router
}

上面實現了五個接口:

  • 發送驗證碼至郵箱: router.post('/verify')
  • 註冊:router.post('/register')
  • 登陸:router.post('/login')
  • 獲取用戶列表:router.get('/alluser')
  • 刪除數據庫中的某個用戶:router.post('/deluser')

分別對應了前面 3.4 中 axios 中的5個請求地址

4.4 JSON Web Token 認證

JSON Web Token(縮寫 JWT)是目前最流行的跨域認證解決方案。詳情參考:http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html

分別建立 /server/token/createToken.js 和 /server/token/checkToken.js

// 建立token
const jwt = require('jsonwebtoken') 

module.exports = function (id) {
  const token = jwt.sign(
    {
      id: id
    },
    'cedric1990',
    {
      expiresIn: '300s'
    }
  )

  return token
}
// 驗證token
const jwt = require('jsonwebtoken')

// 檢查 token
module.exports = async (ctx, next) => {
  // 檢驗是否存在 token
  // axios.js 中設置了 authorization
  const authorization = ctx.get('Authorization')
  if (authorization === '') {
    ctx.throw(401, 'no token detected in http headerAuthorization')
  }

  const token = authorization.split(' ')[1]

  // 檢驗 token 是否已過時
  try {
    await jwt.verify(token, 'cedric1990')
  } catch (err) {
    ctx.throw(401, 'invalid token')
  }

  await next()
}

4.5 服務端入口

根目錄建立 server.js:

// server端啓動入口
const Koa = require('koa')
const app =  new Koa();
const mongoose = require('mongoose')
const bodyParser = require('koa-bodyparser')
const session = require('koa-generic-session')
const Redis = require('koa-redis')
const json = require('koa-json') // 美化json格式化
const dbConfig = require('./server/dbs/config')

const users = require('./server/interface/user.js').router

// 一些session和redis相關配置
app.keys = ['keys', 'keyskeys']
app.proxy = true
app.use(
  session({ 
    store: new Redis()
  })
)

app.use(bodyParser({
  extendTypes: ['json', 'form', 'text']
}))

app.use(json())

// 鏈接數據庫
mongoose.connect(
  dbConfig.dbs,
  { useNewUrlParser: true }
)

mongoose.set('useNewUrlParser', true)
mongoose.set('useFindAndModify', false)
mongoose.set('useCreateIndex', true)

const db = mongoose.connection
mongoose.Promise = global.Promise // 防止Mongoose: mpromise 錯誤

db.on('error', function () {
    console.log('數據庫鏈接出錯')
})

db.on('open', function () {
    console.log('數據庫鏈接成功')
})

// 路由中間件
app.use(users.routes()).use(users.allowedMethods())

app.listen(8888, () => {
  console.log('This server is running at http://localhost:' + 8888)
})

5. 跨域處理

詳情參考:http://www.javashuo.com/article/p-wedrkgsk-gu.html

vue 前端啓動端口9527 和 koa 服務端啓動端口8888不一樣,須要作跨域處理,打開vue.config.js:

devServer: {
    port: 9527,
    https: false,
    hotOnly: false,
    proxy: { 
      '/api': {
        target: 'http://127.0.0.1:8888/', // 接口地址
        changeOrigin: true,
        ws: true,
        pathRewrite: {
          '^/': ''
        }
      }
    }
  }

6. 接口對接

import axios from '../../axios.js'
import CryptoJS from 'crypto-js' // 用於MD5加密處理

發送驗證碼:

// 用戶名不能爲空,而且驗證郵箱格式
sendCode() {
  let email = this.ruleForm2.email
  if (this.checkEmail(email) && this.ruleForm2.username) {  
    axios.userVerify({
      username: encodeURIComponent(this.ruleForm2.username),
      email: this.ruleForm2.email
    }).then((res) => {
      if (res.status === 200 && res.data && res.data.code === 0) {
        this.$notify({
          title: '成功',
          message: '驗證碼發送成功,請注意查收。有效期5分鐘',
          duration: 1000,
          type: 'success'
        })

        let time = 300
        this.buttonText = '已發送'
        this.isDisabled = true
        if (this.flag) {
          this.flag = false;
          let timer = setInterval(() => {
            time--;
            this.buttonText = time + ' 秒'
            if (time === 0) {
              clearInterval(timer);
              this.buttonText = '從新獲取'
              this.isDisabled = false
              this.flag = true;
            }
          }, 1000)
        }
      } else {
        this.$notify({
          title: '失敗',
          message: res.data.msg,
          duration: 1000,
          type: 'error'
        })
      }
    })
  }
}

註冊:

submitForm(formName) {
  this.$refs[formName].validate(valid => {
    if (valid) {
      axios.userRegister({
        username: encodeURIComponent(this.ruleForm2.username),
        password: CryptoJS.MD5(this.ruleForm2.pass).toString(),
        email: this.ruleForm2.email,
        code: this.ruleForm2.smscode
      }).then((res) => {
        if (res.status === 200) {
          if (res.data && res.data.code === 0) {
            this.$notify({
              title: '成功',
              message: '註冊成功。',
              duration: 2000,
              type: 'success'
            })
            setTimeout(() => {
              this.$router.push({
                path: '/login'
              })
            }, 500)
          } else {
            this.$notify({
              title: '錯誤',
              message: res.data.msg,
              duration: 2000,
              type: 'error'
            })
          }
        } else {
          this.$notify({
            title: '錯誤',
            message: `服務器請求出錯, 錯誤碼${res.status}`,
            duration: 2000,
            type: 'error'
          })
        }
      }) 
    } else {
      console.log("error submit!!");
      return false;
    }
  })
},

登陸:

login(formName) {
  this.$refs[formName].validate(valid => {
    if (valid) { 
      axios.userLogin({
        username: window.encodeURIComponent(this.ruleForm.name),
        password: CryptoJS.MD5(this.ruleForm.pass).toString()
      }).then((res) => { 
        if (res.status === 200) {
          if (res.data && res.data.code === 0) {
            this.bindLogin(res.data.token)
            this.saveUser(res.data.username)
            this.$notify({
              title: '成功',
              message: '恭喜,登陸成功。',
              duration: 1000,
              type: 'success'
            })
            setTimeout(() => {
              this.$router.push({
                path: '/'
              })
            }, 500)
          } else {
            this.$notify({
              title: '錯誤',
              message: res.data.msg,
              duration: 1000,
              type: 'error'
            })
          }
        } else {
          this.$notify({
            title: '錯誤',
            message: '服務器出錯,請稍後重試',
            duration: 1000,
            type: 'error'
          })
        }
      })
    }
  })
},

7. 啓動項目 測試接口

7.1 vue端:

$ npm run serve

7.2 啓動mogod:

$ mongod

7.3 啓動Redis:

$ redis-server

7.4 啓動服務端server.js:

安裝 nodemon 熱啓動輔助工具:

$ npm i nodemon
$ nodemon server.js

8. 項目目錄

相關文章
相關標籤/搜索