赞
踩
在上一篇完成了Security登录认证和授权过滤器的编写,后端的登录等功能已经实现。这一篇整合前端Vue实现动态菜单功能。前端Vue项目使用脚手架vue-admin-template
# 克隆项目
git clone https://github.com/PanJiaChen/vue-admin-template.git
# 进入项目目录
cd vue-admin-template
# 安装依赖
npm install
# 建议不要直接使用 cnpm 安装以来,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题
npm install --registry=https://registry.npm.taobao.org
# 启动服务
npm run dev
安装完成后在router.js文件中删除脚手架原来的路由信息,只留下一些基本的路由。
export const constantRoutes = [ { path: '/login', component: () => import('@/views/login/index'), hidden: true }, { path: '/404', component: () => import('@/views/404'), hidden: true }, { path: '/', component: Layout, redirect: '/dashboard', children: [{ path: 'dashboard', name: 'Dashboard', component: () => import('@/views/dashboard/index'), meta: { title: '主页', icon: 'dashboard' } }] }, // 404 page must be placed at the end !!! { path: '*', redirect: '/404', hidden: true } ]
我们需要将原来的登录逻辑和登录接口url的地址修改。首先是在vue.config.js文件中加上代理,使前端项目能够访问到我们的后端接口。
接着找到api/user.js文件中的loginf方法将路径改为我们后台的登录接口,比如我在Security中设置的路径就是**/autoperm/user/login**,请求登录接口后,登录校验成功后台会返回一个token,我们要在邓秋完成后将返回的token存储起来,并且在之后的每次请求都携带。


在request.js文件中修改请求拦截器,每次请求前都在请求头中带上token。

输入错误密码

输入正确密码成功进入主页

后端在请求头中拿到token后获取id,再根据用户id获取菜单列表,把所有菜单查询出来并且筛选出父子菜单以及排序后,构建成前端需要的菜单树。
/** * 构建权限菜单树 * @param req * @return com.lyx.autoperm.utils.R * @author 黎勇炫 * @create 2022/6/21 * @email 1677685900@qq.com */ @GetMapping("/createRouter") public R getRouter(HttpServletRequest req){ // 获取用户id String id = JwtUtils.getMemberIdByJwtToken(req); if(StringUtils.isEmpty(id)){ throw new UserException(UserCodeEnum.TOKEN_NOT_FOUND); } // 根据角色查询所有的菜单 List<Permission> permissions = permissionService.queryPermissionsByRoles(id); // 构建菜单树 List<MenuVO> menus = permissionService.buildMenus(permissions); return R.ok().setData(menus); }
<select id="queryPermissionsDetail" resultType="com.lyx.autoperm.entity.Permission">
select DISTINCT p.* from L_PERMISSION p,L_ROLE_PERMISSION rp where rp.permission_id = p.id and rp.role_id
in
(select r.id from L_USER_ROLE ur,L_ROLE r
<where>
ur.role_id = r.id
<if test="id != null and id !=''">and ur.user_id = #{id}</if>
</where>)
</select>
业务层使用java8的stream流快速处理权限集合,只用十几行代码就能筛选出子菜单列表并排序。
/** * 根据用户查询所有的权限菜单详情 * @param id * @return java.util.Set<com.lyx.autoperm.entity.Permission> * @author 黎勇炫 * @create 2022/6/13 * @email 1677685900@qq.com */ @Override public List<Permission> queryPermissionsByRoles(String id) { // 查询权限列表 List<Permission> permissions = permissionMapper.queryPermissionsDetail(id); // 遍历权限列表,筛选出一级权限 List<Permission> perm = permissions.stream().filter(item -> { return item.getParentId() == 0; }).map(l1 -> { l1.setChildren(findChildren(permissions, l1)); return l1; }).sorted((Comparator.comparingInt(o -> (o.getSort() == null ? 0 : o.getSort())))).collect(Collectors.toList()); return perm; } /** * * @param permissions 权限列表 * @param l1 父权限 * @return java.util.List<com.lyx.autoperm.entity.Permission> * @author 黎勇炫 * @create 2022/6/25 * @email 1677685900@qq.com */ private List<Permission> findChildren(List<Permission> permissions, Permission l1) { List<Permission> children = permissions.stream().filter(perm -> { return perm.getParentId().toString().equals(l1.getId().toString()); }).sorted((Comparator.comparingInt(o -> (o.getSort() == null ? 0 : o.getSort())))).collect(Collectors.toList()); return children; }
拿到正确的菜单列表后,将菜单列表构建成前端需要的菜单树的结构。
/** * 构建前端菜单树 * * @param permissions * @return java.util.List<com.lyx.autoperm.entity.vo.MenuVO> * @author 黎勇炫 * @create 2022/6/20 * @email 1677685900@qq.com */ @Override public List<MenuVO> buildMenus(List<Permission> permissions) { List<MenuVO> menus = new LinkedList<MenuVO>(); // 遍历权限列表,构建菜单 for (Permission item : permissions) { MenuVO menu = new MenuVO(); menu.setHidden(false); menu.setPath(item.getPath()); menu.setComponent(buildComponent(item));item.getType().toString().equals(MenuType.MENU.getCode().toString()); menu.setMeta(new MetaVO(item.getPermName(), item.getIcon())); List<Permission> cMenus = item.getChildren(); // 如果有子菜单 if (!CollectionUtils.isEmpty(cMenus) && cMenus.size() > 0 ) { menu.setAlwaysShow(true); menu.setRedirect("noRedirect"); // 递归调用构建子菜单 menu.setChildren(buildMenus(cMenus)); } else if (item.getParentId().equals(0)) { menu.setMeta(null); List<MenuVO> childrenList = new ArrayList<MenuVO>(); MenuVO children = new MenuVO(); children.setPath(menu.getPath()); children.setComponent(menu.getComponent()); menu.setComponent(ComponentConstant.LAYOUT); children.setName(StringUtils.capitalize(menu.getPath())); children.setMeta(new MetaVO(item.getPermName(), item.getIcon())); childrenList.add(children); menu.setChildren(childrenList); } menus.add(menu); } return menus; }
在permission.js的router.beforeEach中拿到菜单树后,调用router.addRoutes方法添加路由
router.beforeEach(async(to, from, next) => { // start progress bar NProgress.start() // set page title document.title = getPageTitle(to.meta.title) // determine whether the user has logged in const hasToken = getToken() if (hasToken) { if (to.path === '/login') { // if is logged in, redirect to the home page next({ path: '/' }) NProgress.done() } else { const hasGetUserInfo = store.getters.name if (hasGetUserInfo) { next() } else { try { // get user info await store.dispatch('user/getInfo').then(()=>{ // 发起请求,构建路由和菜单 store.dispatch('permission/createRoutes').then(menus=>{ router.addRoutes(menus) // 动态添加可访问路由表 next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 }) }) next() } catch (error) { // remove token and go to login page to re-login await store.dispatch('user/resetToken') Message.error(error || 'Has Error') next(`/login?redirect=${to.path}`) NProgress.done() } } } } else { /* has no token*/ if (whiteList.indexOf(to.path) !== -1) { // in the free login whitelist, go directly next() } else { // other pages that do not have permission to access are redirected to the login page. next(`/login?redirect=${to.path}`) NProgress.done() } } })
src/store/modules/permission.js
import { constantRoutes } from '@/router' import { getRouters } from '@/api/perm' import Layout from '@/layout/index' const state = { routes: [], addRoutes: [], defaultRoutes: [], topbarRouters: [], sidebarRouters: [] } const mutations = { SET_ROUTES: (state, routes) => { state.addRoutes = routes state.routes = constantRoutes.concat(routes) }, SET_DEFAULT_ROUTES: (state, routes) => { state.defaultRoutes = constantRoutes.concat(routes) }, SET_TOPBAR_ROUTES: (state, routes) => { // 顶部导航菜单默认添加统计报表栏指向首页 const index = [{ path: 'index', meta: { title: '统计报表', icon: 'dashboard'} }] state.topbarRouters = routes.concat(index); }, SET_SIDEBAR_ROUTERS: (state, routes) => { state.sidebarRouters = routes }, } const actions= { // 生成路由 createRoutes({ commit }) { return new Promise(resolve => { // 向后端请求路由数据 getRouters().then(res => { console.log(res) const sdata = JSON.parse(JSON.stringify(res.data)) const rdata = JSON.parse(JSON.stringify(res.data)) const sidebarRoutes = filterAsyncRouter(sdata) const rewriteRoutes = filterAsyncRouter(rdata, false, true) rewriteRoutes.push({ path: '*', redirect: '/404', hidden: true }) commit('SET_ROUTES', rewriteRoutes) commit('SET_SIDEBAR_ROUTERS', constantRoutes.concat(sidebarRoutes)) commit('SET_DEFAULT_ROUTES', sidebarRoutes) commit('SET_TOPBAR_ROUTES', sidebarRoutes) resolve(rewriteRoutes) }) }) } } // 遍历后台传来的路由字符串,转换为组件对象 function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) { return asyncRouterMap.filter(route => { if (type && route.children) { route.children = filterChildren(route.children) } if (route.component) { // Layout ParentView 组件特殊处理 if (route.component === 'Layout') { route.component = Layout } else if (route.component === 'ParentView') { route.component = ParentView } else if (route.component === 'InnerLink') { route.component = InnerLink } else { console.log(route.component) route.component = loadView(route.component) } } if (route.children != null && route.children && route.children.length) { route.children = filterAsyncRouter(route.children, route, type) } else { delete route['children'] delete route['redirect'] } return true }) } function filterChildren(childrenMap, lastRouter = false) { var children = [] childrenMap.forEach((el, index) => { if (el.children && el.children.length) { if (el.component === 'ParentView') { el.children.forEach(c => { c.path = el.path + '/' + c.path if (c.children && c.children.length) { children = children.concat(filterChildren(c.children, c)) return } children.push(c) }) return } } if (lastRouter) { el.path = lastRouter.path + '/' + el.path } children = children.concat(el) }) return children } export const loadView = (view) => { // 路由懒加载 return (resolve) => require([`@/views${view}`], resolve) } export default { namespaced: true, state, mutations, actions }
src/layout/components/Sidebar/index.vue

数据库中菜单列表以及和角色对应的菜单,我所登录的角色编号是2.


登录成功后的菜单列表,菜单是根据数据库的信息动态生成的。

源码地址
SpringBoot+SpringSecurity+Vue实现动态权限(一)
SpringBoot+SpringSecurity+Vue实现动态权限(二)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。