美文网首页vue
express mysql vue element-ui实现通用

express mysql vue element-ui实现通用

作者: wangwenquan1234 | 来源:发表于2020-04-28 10:04 被阅读0次

    环境参数

    前端

    • 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交流学习。

    相关文章

      网友评论

        本文标题:express mysql vue element-ui实现通用

        本文链接:https://www.haomeiwen.com/subject/fommwhtx.html