vue

express mysql vue element-ui实现通用

2020-04-28  本文已影响0人  wangwenquan1234

环境参数

前端

后端

数据库

数据库设计

image.png

环境搭建

项目搭建没有什么特别的地方,都是拿官方提供的脚手架生成的

接口安全

由于前后端分离的项目是独立开发独立发布的,会涉及到跨域的问题,有两种方式解决

我这里采取了一种方式,这种方式的好处是简单除暴,缺点是安全性较差。
我这里的约定方式是前端发送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交流学习。

上一篇下一篇

猜你喜欢

热点阅读