本文章是学习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('登录成功')
})
登录校验完成,但仅仅是校验了密码,我们还需要对登录身份进行处理。我们要每次登录都了解是哪个用户访问我们的系统,那么我们就要使用到token验证技术。
每次请求头上都携带一个先前通过服务器验证通过后发放的token令牌。这样下次访问接口的时候服务器就不需要密码,直接给你提供信息了。
要怎么生成TOKEN令牌呢,我们这里使用了jsonwebtoken
简称JWT标准。没有了解过的,可以参考这篇阮一峰大神的博客- JSON Web Token 入门教程。
要理解token
是什么,我觉得可以把它想象成 演唱会门票,演唱会门票就是主办方发放给你的Token,它上面携带了使用时间( exp 过期时间
),它上面还携带了你的信息(payload
),当然它也拥有对应的校验方法以防伪造。
那么理解了它的使用,就可以直接使用jsonwebtoken
了。
它的流程无非就是:
登陆成功服务端发放token–》客户端保存token–》客户端发送token校验身份–》服务端验证放行。
jsonwebtoken
npm i -D jsonwebtoken
/**
* 登录接口
* @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})
})
前端需要把接收到的token保存起来,可以保存在cookies
或者localStorage
中,我们这里使用localStorage
。
const res = await this.$http.post('/login', this.model)
// 获取登录后的token验证,Bearer为jsonwebtoken的行业标准。
localStorage.setItem("token", `Bearer ${res.data.token}`)
这样前端就可以使用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') || ''
}
}
}
})
在每个请求前增加一个流程,先验证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}}
到此,后台管理系统的所有基本功能都已经完成了,这一路学习下来,真是学习了不少,感谢老师的教程让我看到很多对于我来说很有用的知识!!
继续努力,继续学习老师的前端页面部分实现。