基于 Vue + Koa2 + MongoDB + Redis 实现一个完整的登录注册
基于 Vue + Koa2 + MongoDB + Redis 实现一个完整的登录注册
项目地址:https://github.com/caochangkui/vue-element-responsive-demo/tree/login-register
通过 vue-cli3.0 + Element 构建项目前端,Node.js + Koa2 + MongoDB + Redis 实现数据库和接口设计,包括邮箱验证码、用户注册、用户登录、查看删除用户等功能。
1. 技术栈
-
前端
- 初始化项目:vue-cli3.0
- 组件库:Element-ui
- 路由控制/拦截:Vue-router
- 状态管理:Vuex
-
服务端
- 运行环境:Node.js
- 后台开发框架:Koa2
- 路由中间件:Koa-router
- 发送邮件: nodemailer
-
HTTP通讯
- 接口请求/拦截:Axios
- Token认证:jsonwebtoken
-
数据库
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 构建项目前端页面
登录页(@/view/users/Login.vue):
注册页(@/view/users/Register.vue):
发送验证码前需要验证用户名和邮箱,用户名必填,邮箱格式需正确。
用户设置页(@/view/users/setting/Setting.vue)
用户登录后,可以进入用户设置页查看用户和删除用户
3.2 Vuex 状态管理
通过 vuex 实现保存或删除用户 token,保存用户名等功能。
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。
为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter。
根目录下新建store文件夹,创建modules/user.js:
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 对数据进行处理输出:
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 路由拦截参考:https://www.cnblogs.com/cckui/p/10319013.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: '请填写验证码'
}