环境参数
前端
- vue 2.6.10
- vue-router 3.0.3
- vuex 3.0.1
- element-ui 2.12.0
后端
- express
数据库
- mysql 5.7
数据库设计
image.png环境搭建
项目搭建没有什么特别的地方,都是拿官方提供的脚手架生成的
- 前端 vue/cli3
- 后端 express-generator
接口安全
由于前后端分离的项目是独立开发独立发布的,会涉及到跨域的问题,有两种方式解决
- 后端开启跨域
- 后端不开跨域,前端开发环境做使用webpack proxy方式做反向代理,生产环境用Nginx做反向代理
我这里采取了一种方式,这种方式的好处是简单除暴,缺点是安全性较差。
我这里的约定方式是前端发送http请求的时候,默认带一个sign
参数,它是一个时间戳,后端接收并验证它的合法性。
需要注意的是,我这种方式比较简单,更可靠的方式是让加密这个sign
参数,这样可以最大限度的保证接口安全
前端传递签名
// http 默认参数
const sign = new Date().valueOf()
/**
* 请求拦截器
*/
const requestConfig = config => {
// 向服务端携带的默认参数
config.params = {
...config.params,
sign
}
return config
}
后端接收签名并验证
// sign验证
app.all('/*', function (req, res, next) {
const { sign } = req.query || req.body
if (sign === undefined) {
res.json(new ErrorModel('sign 0'))
return
}
const _sign = new Date().valueOf()
const result = parseInt(_sign - sign)
if (isNaN(result)) {
res.json(new ErrorModel('sign 1'))
return
}
// 大于一分钟
if (result / (1000 * 60) > 60) {
res.json(new ErrorModel('sign 2'))
return
}
next()
});
动态路由
一个后台管理系统,不同的用户的权限是不一样的,由于这个项目采用前后端分离的方式来开发,因此,为了安全性,前端在创建路由的时候,不能讲将所有的路由全部创建出来,它应该是根据接口返回是数据动态生成的。我的实现方式如下
接口 getAllMenu
{
"data": [
{
"pkid": 1,
"parent_id": 0,
"menu_name": "权限管理",
"menu_icon": "el-icon-tickets",
"menu_url": "auth-management",
"level": 0,
"children": [
{
"pkid": 2,
"parent_id": 1,
"menu_name": "用户管理",
"menu_icon": null,
"menu_url": "user-management",
"level": 1,
"children": []
},
{
"pkid": 3,
"parent_id": 1,
"menu_name": "菜单管理",
"menu_icon": null,
"menu_url": "menu-management",
"level": 1,
"children": []
},
{
"pkid": 4,
"parent_id": 1,
"menu_name": "角色管理",
"menu_icon": null,
"menu_url": "role-management",
"level": 1,
"children": []
}
]
},
{
"pkid": 5,
"parent_id": 0,
"menu_name": "栏目管理",
"menu_icon": "el-icon-folder-opened",
"menu_url": "cate-management",
"level": 0,
"children": []
},
{
"pkid": 6,
"parent_id": 0,
"menu_name": "内容管理",
"menu_icon": "el-icon-folder-opened",
"menu_url": "content-management",
"level": 0,
"children": [
{
"pkid": 7,
"parent_id": 6,
"menu_name": "产品",
"menu_icon": "",
"menu_url": "product",
"level": 1,
"children": []
},
{
"pkid": 8,
"parent_id": 6,
"menu_name": "文章",
"menu_icon": "",
"menu_url": "article",
"level": 1,
"children": []
}
]
},
{
"pkid": 9,
"parent_id": 0,
"menu_name": "留言板管理",
"menu_icon": "el-icon-folder-opened",
"menu_url": "message-board-management",
"level": 0,
"children": []
},
{
"pkid": 10,
"parent_id": 0,
"menu_name": "友情链接管理",
"menu_icon": "el-icon-folder-opened",
"menu_url": "friend-link-management",
"level": 0,
"children": []
},
{
"pkid": 11,
"parent_id": 0,
"menu_name": "站点信息管理",
"menu_icon": "el-icon-folder-opened",
"menu_url": "site-info-management",
"level": 0,
"children": []
}
],
"message": "SUCCESS",
"code": 200
}
接口 getRoleMenuListByUserId
{
"data": {
"pkid": 1,
"fk_role_id": 1,
"role_menu_list": "[1,2,3,4,5,6,7,8,9]"
},
"message": "SUCCESS",
"code": 200
}
// 1.查询所有菜单
post('/menu/getAllMenu', {}).then(res => {
// 获取所有菜单
const treeMenuList = res.data
// 校验菜单
if (!treeMenuList.length) {
return Message.error('菜单为空,请联系管理员添加')
}
// 2.根据用户ID查询该用户有权限的菜单
post('/roleMenu/getRoleMenuListByUserId', {
userId: parseInt(store.state.userId)
}).then(res => {
// 获取权限菜单id
const menuKeys = JSON.parse(res.data.role_menu_list)
// 3.将menuList转为一维数组
const arrayMenuList = treeToArray(treeMenuList)
// 4.通过主键id进行过滤
const filterArrayMenuList = filterArrayByMenuId(arrayMenuList, menuKeys)
// 5.将过滤好的一位数组转成tree
const filterTreeMenuList = arrayToTree(filterArrayMenuList, 0)
// 6.将处理好的tree转成vue-router数据格式
const resultData = transformHttpDataToVueRouterData(filterTreeMenuList)
// 7.将数据存到sessionStorage中
sessionStorage.setItem('router', JSON.stringify(resultData))
// 8.派发到store中
store.dispatch('menuList', {
menuList: resultData
})
// 9.创建动态路由
routerGo(to, next)
router.replace({ name: 'Home' })
})
})
/**
* 添加路由
* @param to
* @param next
*/
function routerGo (to, next) {
// 获取sessionStorage中的router字段,并序列化
const routerData = JSON.parse(sessionStorage.getItem('router'))
// console.log(routerData)
// layout组件是主页布局文件,需要手动引入
const asyncRouter = [
{
name: 'Layout',
path: '/layout',
component: () => import('@/component/layout/Layout.vue'),
children: []
}
]
// 通过componentPath创建component组件
asyncRouter[0].children = transformVueRouterDataToVueRouterComponent(routerData)
// 添加Home组件
asyncRouter[0].children.push({
path: 'home',
name: 'Home',
component: () => import('@/view/home/Home.vue')
})
// 在路由末尾添加404
asyncRouter.push({
path: '*',
name: 'notFount',
component: () => import('@/view/not-fount/NotFound.vue')
})
// 添加动态路由
router.addRoutes(asyncRouter)
next({ ...to, replace: true })
}
/**
* 接口列表格式转换成满足vue-router的对应字段
* @param data
* @param array
* @param str
* @returns {Array}
*/
function transformHttpDataToVueRouterData (data, array = [], str = '/') {
data.forEach((item, index) => {
array.push({
menuId: item.pkid,
label: item.menu_name,
path: item.menu_url,
name: toCamel(item.menu_url),
icon: item.menu_icon,
// 这个字段是用来做浏览器地址链接有用的
componentPath: `${str}${item.menu_url}`,
children: []
})
if (item.children && item.children.length) {
array[index].redirect = {
name: toCamel(item.children[0].menu_url)
}
transformHttpDataToVueRouterData(item.children, array[index].children, `${array[index].componentPath}/`)
} else {
array[index].redirect = null
}
})
return array
}
/**
* 将component字段转成component组件
* @param root
* @returns {*}
*/
function transformVueRouterDataToVueRouterComponent (root) {
root.forEach((item) => {
let path = item.componentPath
path = path + '/' + toCamel(path.substring(path.lastIndexOf('/') + 1))
// 因为webpack引入import机制的问题。全部转成变量不能解析
// item.component = () => import(`./view${path}.vue`)
item.component = () => import('./view' + path + '.vue')
if (item.children && item.children.length) {
transformVueRouterDataToVueRouterComponent(item.children)
}
})
return root
}
/**
* 中划线命名转大驼峰命名
* @param str
* @returns {*}
*/
function toCamel (str) {
str = str.replace(/(\w)/, (match, $1) => `${$1.toUpperCase()}`)
while (str.match(/\w-\w/)) {
str = str.replace(/(\w)(-)(\w)/, (match, $1, $2, $3) => `${$1}${$3.toUpperCase()}`)
}
return str
}
JWT方式实现登录
我这里依赖了两个包express-jw和express,实现方式如下
后端
// app.js
const expressJwt = require('express-jwt')
app.use(expressJwt({
secret: SECRET_KEY
}).unless({
path: [
'/api/user/login'
]
}))
// error handler
app.use(function (err, req, res, next) {
console.log(err);
if (err.name === 'UnauthorizedError') {
res.json(new TokenModel(err))
return
}
// render the error page
res.status(err.status || 500);
res.render('error');
});
// user.js
router.post('/login', function (req, res, next) {
// 获取参数
let { username, password } = req.body || req.query
// 加密password
password = genPassword(password)
// 防止sql注入
username = escape(username)
password = escape(password)
// 校验字段名
if (username === undefined) {
res.json(new ErrorModel('参数 username 是必须的'))
return
}
if (password === undefined) {
res.json(new ErrorModel('参数 password 是必须的'))
return
}
// 校验字段类型
if (typeof username !== 'string') {
res.json(new ErrorModel('参数 username 必须是 string'))
return
}
if (typeof password !== 'string') {
res.json(new ErrorModel('参数 password 必须是 string'))
return
}
// 校验字段
if (username.trim().length === 0) {
res.json(new ErrorModel('参数 username 必须有值'))
return
}
if (password.trim().length === 0) {
res.json(new ErrorModel('参数 password 必须有值'))
return
}
// sql
const sql = `
select pkid, username, real_name from t_user where username = ${username} and password = ${password};
`
exec(sql).then(result => {
if (result.length) {
const username = result[0].username
let token = jwt.sign({ username: username }, SECRET_KEY, { expiresIn: 60 * 60 * 12 })
result[0].token = token
res.json(new SuccessModel(result[0]))
} else {
res.json(new ErrorModel('登录失败,用户名或密码错误。'))
}
}).catch(err => {
console.error('数据库异常 ', err)
res.json(new ErrorModel(err, '数据库异常'))
})
})
项目演示地址
http://129.204.109.68
欢迎大家关注我的公众号:爆笑程序员 回复 学习资源 可以领取某课网付费视频学习资源一份 。
欢迎大家加我的微信w80944188交流学习。
网友评论