Vue实战笔记04 - 简易网站后台的搭建之登录系统的搭建

后台登录系统的搭建

本文章是学习B站全栈之巅系列课程的学习笔记,利用笔记来吸收升华学到的知识。

学习感受是,如果有一定的nodejs+mongoDB+vue的基础,加做过一两个相关的练手项目,再学习这个课程,你会发现原来搭建一个完整有基础功能的网站,是如此的简单,会有很大的收获。
参考源码:https://github.com/morningClock/herohoner
视频学习地址:点击这里

管理员管理功能

1.在之前的基础,新增管理员账号列表页与新建管理员的页面。

2.修改请求的接口名称admin_users

3.后端新增AdminUser的数据模型。

这样就可以对管理员账号进行增删改查操作了。

加密

现在所有密码都是明文存储,在实际项目中,为了保护用户的隐私,我们在任何地方都不应该明文存储密码。我们应该要先加密后再存储到数据库,防止数据库泄漏导致用户隐私泄漏。

传统做法是使用加密算法进行加密,可以使用MD5,但是这种加密可以通过一定的手段破解。

所以为了更加安全,我们可以使用bcrypt进行密码的加密。

它的特点是:同样的密码,每一次加密的结果都不会一样。

在服务端安装bcrypt插件

npm i -D bcrypt

我们只需要存入数据库之前对密码加一次密就ok了。所以我们可以直接在数据模型中,对密码进行转换。

使用set方法,可以先对数据进行方法处理。

const mongoose = require('mongoose')

const schema = new mongoose.Schema({
  username: { type: String },
  name: { type: String },
  password: { 
    type: String,
    set(val){
      // set 自定义数据处理,此处用bcrypt进行密码散列加密
      // 每次散列加密的效果不一样
      // 参数1:加密字符串,参数2:加密等级(次数)
      return require('bcrypt').hashSync(val, 10)
    }
  }
})

module.exports = mongoose.model('AdminUser', schema)

这时我们去新增管理员,就可以看到,密码明文的被打印在前端的输入框中。

如果在保存,密码又会被再次加密,这样密码就不一样了。我们希望前端看不见密码,且默认不打印出来。

这时候我们就需要用到schma的select选项属性。

它可以默认不会被查询出来,除非特指要查询出来。

const mongoose = require('mongoose')

const schema = new mongoose.Schema({
  username: { type: String },
  name: { type: String },
  password: { 
    type: String,
    // 不返回该字段查询结果
    select: false,
    set(val){
      // set 自定义数据处理,此处用bcrypt进行密码散列加密
      // 每次散列加密的效果不一样
      // 参数1:加密字符串,参数2:加密等级(次数)
      return require('bcrypt').hashSync(val, 10)
    }
  }
})

module.exports = mongoose.model('AdminUser', schema)

此时,查询到的管理员详情,不会查出密码password字段。

如果非要查出密码字段,只需要设置select为true

或者在查询时增加查询语句。

AdminUser.findOne({username}).select('+password')

新增登录页面

有了管理员账号,我们就需要增加登录页面了,使用Element来简单搭建一个登录表单。





建立好静态页面后,我们就要将账号密码提交到后端,进行密码的校验工作。

其中校验使用到了,bcrypt中的校验方法compareSync进行密码校验,其中Sync是同步请求方法。

bcrypt.compareSync('需要校验的密码', '比对密码')

后端新增接口/login

/** 
  * 登录接口
  * @POST  /admin/api/login
  * @param  {[username, password]}
  * @return {[]}
  */
  app.post('/admin/api/login', async (req, res) => {
    const {username, password} = req.body;
    const AdminUser = require('../../models/AdminUser')
    // 取出默认不取出的password值
    const user = await AdminUser.findOne({username}).select('+password')
    // 1.根据用户查询是否存在用户
    if (!user) {
      return res.status(421).send({message:'用户不存在'})
    }
    // 2.校验密码
    const isValid = require('bcrypt').compareSync(password, user.password)
    if (!isValid) {
      return res.status(422).send({message:'密码错误'})
    }
    // 3. 登录成功
    return res.status(200).send('登录成功')
  })

JWT验证登录

登录校验完成,但仅仅是校验了密码,我们还需要对登录身份进行处理。我们要每次登录都了解是哪个用户访问我们的系统,那么我们就要使用到token验证技术。

每次请求头上都携带一个先前通过服务器验证通过后发放的token令牌。这样下次访问接口的时候服务器就不需要密码,直接给你提供信息了。

要怎么生成TOKEN令牌呢,我们这里使用了jsonwebtoken简称JWT标准。没有了解过的,可以参考这篇阮一峰大神的博客- JSON Web Token 入门教程。

要理解token是什么,我觉得可以把它想象成 演唱会门票,演唱会门票就是主办方发放给你的Token,它上面携带了使用时间( exp 过期时间),它上面还携带了你的信息(payload),当然它也拥有对应的校验方法以防伪造。

那么理解了它的使用,就可以直接使用jsonwebtoken了。

它的流程无非就是:

登陆成功服务端发放token–》客户端保存token–》客户端发送token校验身份–》服务端验证放行。

1.安装jsonwebtoken

npm i -D jsonwebtoken

2.登陆发放token

/** 
  * 登录接口
  * @POST  /admin/api/login
  * @param  {[username, password]}
  * @return {[token]}
  */
  app.post('/admin/api/login', async (req, res) => {
    const {username, password} = req.body;
    const AdminUser = require('../../models/AdminUser')
    // 取出默认不取出的password值
    const user = await AdminUser.findOne({username}).select('+password')
    // 1.根据用户查询是否存在用户
    if (!user) {
      return res.status(421).send({message:'用户不存在'})
    }
    // 2.校验密码
    const isValid = require('bcrypt').compareSync(password, user.password)
    if (!isValid) {
      return res.status(422).send({message:'密码错误'})
    }
    // 3. 登录成功,返回token
    const rules = {
      id: user._id
    }
    // 这里的加密方法,secret字串,我们提取到一个公共的config.js中管理。
    const token = await jwt.sign(rules, keys.secret, { expiresIn: 60 * 60 })
    return res.status(200).send({token: token, 'name': user.username})
  })

3.客户端保存token

前端需要把接收到的token保存起来,可以保存在cookies或者localStorage中,我们这里使用localStorage

const res = await this.$http.post('/login', this.model)
// 获取登录后的token验证,Bearer为jsonwebtoken的行业标准。
localStorage.setItem("token", `Bearer ${res.data.token}`)

这样前端就可以使用token进行请求接口了。

4.客户端携带token进行请求

此处我们直接使用axios封装的请求拦截功能。

// 添加请求拦截器
http.interceptors.request.use(function (config) {
  // 发送请求前,携带token
  // console.log('请求前')
  config.headers.Authorization = localStorage.getItem('token')
  return config;
}, function (error) {
  // 对请求错误做些什么
  return Promise.reject(error);
});

当然这里只对axios请求进行了拦截处理,我们使用普通的请求提交表单是不会携带这个token的,所以会有问题。比如我们的上传图标,就使用了传统的表单提交,所以我们要简单的处理一下。


    
    

getAuthHeader () {
    return {
        Authorization: localStorage.getItem('token') || ''
    }
}

我们发现,如果我们要每一处上传图片都写一个同样的获取方法,那样冗余太多,也不利于维护。所以我们要提取这个方法,放到全局上。共享方法有很多方案,我们这里跟着老师使用mixin方法混入默认方法中。

在main.js中设置,将方法混入默认的方法中,所有vue示例对象都会默认拥有这个方法。

// 全局混入方法
Vue.mixin({
  methods:{
    getAuthHeader () {
      return {
        Authorization: localStorage.getItem('token') || ''
      }
    }
  }
})

5.服务端验证令牌

在每个请求前增加一个流程,先验证jwt,通过之后放行,

async (req, res, next) => {
    const token = (req.headers.authorization).split(' ').pop()
    // 校验token
    try {
        req.user = jwt.verify(token, keys.secret);
        // 找到用户相关信息,并返回
        // 继续执行接口
        next()
    }
    catch(err) {
        res.status(402).send({message:'token 无效'})
    }
}

但是要每个接口都添加同样的方法,这过于冗余。我们就可以考虑把它封装成为中间件。

创建midleware文件夹管理自定义中间件,创建auth.js

const jwt = require('jsonwebtoken') 
const keys = require('../config/keys')

module.exports = (options) => {
  return async (req, res, next) => {
    const token = (req.headers.authorization).split(' ').pop()
    // 校验token
    try {
      req.user = jwt.verify(token, keys.secret);
      // 找到用户相关信息,并返回
      // 继续执行接口
      next()
    }
    catch(err) {
      res.status(402).send({message:'token 无效'})
    }
  }
}

接口文件中,执行中间件方法。

此处也罢resource(引入对应model),封装成中间件了瞬间简洁了。

其余全局非公开的api,都要添加这个auth进行验证

// 自定义middleware
// 校验
const authMiddleware = require('../../middleware/auth')
// 通用接口定义:根据resource请求不同接口
  app.use('/admin/api/rest/:resource', authMiddleware(), resourceMiddleware(), router)

前端响应时,如果发生错误,就是token不通过,则跳转回login页面,并清空原来的token

// 添加响应拦截器
http.interceptors.response.use(function (response) {
  // 对响应数据做点什么
  // console.log('请求后')
  return response;
}, function (error) {
  if(error.response.data.message){
    Vue.prototype.$message.error(error.response.data.message)
  }
  // 清空token
  localStorage.removeItem('token')
  router.push({path: '/login'})
  return Promise.reject(error);
});

前端权限路由

当然后端完成了权限接口,前端不做限制,也可以访问对应路径,只是没有数据而已。

此时我们也需要对前端进行路由的权限设置。

前端路由文件router.js中,使用meta元信息标记公开访问的页面,予以放行。

{
    path: '/login',
    component: Login,
    meta: { isPublic: true }
},

并添加路由守卫,进行访问权限的限制。

router.beforeEach((to, from, next) => {
  // to.meta.isPublic表示是否能公开访问
  // 没有token时,限制访问非公开页面
  // 记得执行完后return,不然下面的语句照样执行
  if (!to.meta.isPublic && !localStorage.getItem('token')) {
    Vue.prototype.$message.error('请先登录')
    return next('/login')
  }
  // 如果已经登陆,禁止访问登录页
  if (to.path == '/login' && localStorage.getItem('token')) {
    Vue.prototype.$message.success('登录成功')
    return next(from.path)
  }
  return next()
})

当然我们还要完善登录页面login.vue的跳转功能。

async doLogin () {
    const res = await this.$http.post('/login', this.model)
    // 获取登录后的token验证
    localStorage.setItem("token", `Bearer ${res.data.token}`)
    localStorage.setItem("name", res.data.name)
    // 跳转到管理系统
    this.$message.success('登录成功')
    this.$router.push('/')
}

完善

增加退出登录功能

Main.vue中的下拉列表改造


    
        
        
            
                
            
        
    
    {{name}}


到此,后台管理系统的所有基本功能都已经完成了,这一路学习下来,真是学习了不少,感谢老师的教程让我看到很多对于我来说很有用的知识!!

继续努力,继续学习老师的前端页面部分实现。

你可能感兴趣的:(实战项目,Vue,登录系统)