• Vue 动态路由的实现以及 Springsecurity 按钮级别的权限控制


    思路:

    动态路由实现:在导航守卫中判断用户是否有用户信息,通过调用接口,拿到后台根据用户角色生成的菜单树,格式化菜单树结构信息并递归生成层级路由表并使用Vuex保存,通过 router.addRoutes 动态挂载到 router 上,按钮级别的权限控制,则需使用自定义指令去实现。

    实现:

    导航守卫代码:

    复制代码
    router.beforeEach((to, from, next) => {
      NProgress.start() // start progress bar
      to.meta && (typeof to.meta.title !== 'undefined' && setDocumentTitle(`${to.meta.title} - ${domTitle}`))
      if (getStore('ACCESS_TOKEN')) {
        /* has token */
        if (to.path === '/user/login') {
          next({ path: '/other/list/user-list' })
          NProgress.done()
        } else {
          if (store.getters.roles.length === 0) {
            store
              .dispatch('GetInfo')
              .then(res => {
                const username = res.principal.username
                store.dispatch('GenerateRoutes', { username }).then(() => {
                  // 根据roles生成可访问的路由表
                  // 动态添加可访问路由表
                  router.addRoutes(store.getters.addRouters)
                  const redirect = decodeURIComponent(from.query.redirect || to.path)
                  if (to.path === redirect) {
                    // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
                    next({ ...to, replace: true })
                  } else {
                    // 跳转到目的路由
                    next({ path: redirect })
                  }
                })
              })
              .catch(() => {
                notification.error({
                  message: '错误',
                  description: '请求用户信息失败,请重试'
                })
                store.dispatch('Logout').then(() => {
                  next({ path: '/user/login', query: { redirect: to.fullPath } })
                })
              })
          } else {
            next()
          }
        }
      } else {
        if (whiteList.includes(to.name)) {
          // 在免登录白名单,直接进入
          next()
        } else {
          next({ path: '/user/login', query: { redirect: to.fullPath } })
          NProgress.done() // if current page is login will not trigger afterEach hook, so manually handle it
        }
      }
    })
    复制代码

     Vuex保存routers

    复制代码
    const permission = {
      state: {
        routers: constantRouterMap,
        addRouters: []
      },
      mutations: {
        SET_ROUTERS: (state, routers) => {
          state.addRouters = routers
          state.routers = constantRouterMap.concat(routers)
        }
      },
      actions: {
        GenerateRoutes ({ commit }, data) {
          return new Promise(resolve => {
            generatorDynamicRouter(data).then(routers => {
              commit('SET_ROUTERS', routers)
              resolve()
            })
          })
        }
      }
    }
    复制代码

     路由工具

    访问后端接口获得菜单树,然后对菜单树进行处理,把菜单树的组件字符串进行转换为前端的组件如:

    userlist: () => import('@/views/other/UserList'),这样生成的路由就是我们所要的了。

    复制代码
    import { axios } from '@/utils/request'
    import { UserLayout, BasicLayout, RouteView, BlankLayout, PageView } from '@/layouts'
    
    // 前端路由表
    const constantRouterComponents = {
      // 基础页面 layout 必须引入
      BasicLayout: BasicLayout,
      BlankLayout: BlankLayout,
      RouteView: RouteView,
      PageView: PageView,
    
      // 需要动态引入的页面组件
      analysis: () => import('@/views/dashboard/Analysis'),
      workplace: () => import('@/views/dashboard/Workplace'),
      monitor: () => import('@/views/dashboard/Monitor'),
      userlist: () => import('@/views/other/UserList')
      // ...more
    }
    
    // 前端未找到页面路由(固定不用改)
    const notFoundRouter = {
      path: '*', redirect: '/404', hidden: true
    }
    
    /**
     * 获取后端路由信息的 axios API
     * @returns {Promise}
     */
    export const getRouterByUser = (parameter) => {
      return axios({
        url: '/menu/' + parameter.username,
        method: 'get'
      })
    }
    
    /**
     * 获取路由菜单信息
     *
     * 1. 调用 getRouterByUser() 访问后端接口获得路由结构数组
     * 2. 调用
     * @returns {Promise<any>}
     */
    export const generatorDynamicRouter = (data) => {
      return new Promise((resolve, reject) => {
        // ajax
        getRouterByUser(data).then(res => {
          // const result = res.result
          const routers = generator(res)
          routers.push(notFoundRouter)
          resolve(routers)
        }).catch(err => {
          reject(err)
        })
      })
    }
    
    /**
     * 格式化 后端 结构信息并递归生成层级路由表
     *
     * @param routerMap
     * @param parent
     * @returns {*}
     */
    export const generator = (routerMap, parent) => {
      return routerMap.map(item => {
        const currentRouter = {
          // 路由地址 动态拼接生成如 /dashboard/workplace
          path: `${item && item.path || ''}`,
          // 路由名称,建议唯一
          name: item.name || item.key || '',
          // 该路由对应页面的 组件
          component: constantRouterComponents[item.component || item.key],
          // meta: 页面标题, 菜单图标, 页面权限(供指令权限用,可去掉)
          meta: { title: item.name, icon: item.icon || undefined, permission: item.key && [ item.key ] || null }
        }
        // 为了防止出现后端返回结果不规范,处理有可能出现拼接出两个 反斜杠
        currentRouter.path = currentRouter.path.replace('//', '/')
        // 重定向
        item.redirect && (currentRouter.redirect = item.redirect)
        // 是否有子菜单,并递归处理
        if (item.children && item.children.length > 0) {
          // Recursion
          currentRouter.children = generator(item.children, currentRouter)
        }
        return currentRouter
      })
    }
    复制代码

    后端菜单树生成工具类

    复制代码
    /**
     * 构造菜单树工具类
     * @author dang
     *
     */
    public class TreeUtil {
    
        protected TreeUtil() {
    
        }
    
        private final static Long TOP_NODE_ID = (long) 1;
        /**
         * 构造前端路由
         * @param routes
         * @return
         */
        public static ArrayList<MenuEntity> buildVueRouter(List<MenuEntity> routes) {
            if (routes == null) {
                return null;
            }
            List<MenuEntity> topRoutes = new ArrayList<>();
            routes.forEach(route -> {
                Long parentId = route.getParentId();
                if (TOP_NODE_ID.equals(parentId)) {
                    topRoutes.add(route);
                    return;
                }
                for (MenuEntity parent : routes) {
                    Long id = parent.getId();
                    if (id != null && id.equals(parentId)) {
                        if (parent.getChildren() == null) {
                            parent.initChildren();
                        }
                        parent.getChildren().add(route);
                        return;
                    }
                }
            });
    
            ArrayList<MenuEntity> list = new ArrayList<>();
            MenuEntity root = new MenuEntity();
            root.setName("首页");
            root.setComponent("BasicLayout");
            root.setPath("/");
            root.setRedirect("/other/list/user-list");
            root.setChildren(topRoutes);
            list.add(root);
            return list;
        }
    }
    复制代码

    菜单实体 (使用了lombok插件)

    复制代码
    /**
     * 菜单实体
     * @author dang
     *
     */
    
    public class MenuEntity extends CoreEntity {
    
        private static final long serialVersionUID = 1L;
        @TableField("FParentId")
        private Long parentId;
        @TableField("FNumber")
        private String number;
        @TableField("FName")
        private String name;
        @TableField("FPerms")
        private String perms;
        @TableField("FType")
        private int type;
        @TableField("FLongNumber")
        private String longNumber;
        @TableField("FPath")
        private String path;
        @TableField("FComponent")
        private String component;
        @TableField("FRedirect")
        private String redirect;
        
        @TableField(exist = false)
        private List<MenuEntity> children;
        @TableField(exist = false)
        private MenuMeta meta;
        @TableField(exist = false)
        private List<PermissionEntity> permissionList;
        
        @Override
        public int hashCode() {
            return number.hashCode();
        }
        
        @Override
        public boolean equals(Object obj) {
            return super.equals(obj(obj);
        }
    
        public void initChildren() {
            
            this.children = new ArrayList<>();
        }
    }
    复制代码

    路由菜单是根据用户的角色去获得的,一个用户具有多个角色,一个角色具有多个菜单

    钮权限控制实现思路:

    说下按钮权限控制的实现:前端vue主要用自定义指令实现控制按钮的显示与隐藏,后端我用的是SpringSecurity框架,所以使用的是@PreAuthorize注解,在菜单实体的perms属性记录权限的标识,如:sys:user:add,记录有权限标识的菜单其 parentId 应为上级菜单,然后获取用户的perms集合,在用户登录的时候传给前端并用Vuex保存,在自定义指令中去比较用户是否含有按钮所需要的权限。

    实现:

    获取用户信息的时候,把权限存到Vuex中 commit('SET_PERMISSIONS', result.authorities)

    复制代码
      // 获取用户信息
        GetInfo ({ commit }) {
          return new Promise((resolve, reject) => {
            getInfo().then(response => {
              const result = response
              if (result.authorities) {
                commit('SET_PERMISSIONS', result.authorities)
                commit('SET_ROLES', result.principal.roles)
                commit('SET_INFO', result)
              } else {
                reject(new Error('getInfo: roles must be a non-null array !'))
              }
              commit('SET_NAME', { name: result.principal.displayName, welcome: welcome() })
              commit('SET_AVATAR', result.principal.avatar)
              resolve(response)
            }).catch(error => {
              reject(error)
            })
          })
        }
    复制代码

    前端自定义指令

    复制代码
    // 定义一些和权限有关的 Vue指令
    // 必须包含列出的所有权限,元素才显示
    export const hasPermission = {
      install (Vue) {
        Vue.directive('hasPermission', {
          bind (el, binding, vnode) {
            const permissions = vnode.context.$store.state.user.permissions
            const per = []
            for (const v of permissions) {
              per.push(v.authority)
            }
            const value = binding.value
            let flag = true
            for (const v of value) {
              if (!per.includes(v)) {
                flag = false
              }
            }
            if (!flag) {
              if (!el.parentNode) {
                el.style.display = 'none'
              } else {
                el.parentNode.removeChild(el)
              }
            }
          }
        })
      }
    }
    // 当不包含列出的权限时,渲染该元素
    export const hasNoPermission = {
      install (Vue) {
        Vue.directive('hasNoPermission', {
          bind (el, binding, vnode) {
            const permissions = vnode.context.$store.state.user.permissions
            const per = []
            for (const v of permissions) {
              per.push(v.authority)
            }
            const value = binding.value
            let flag = true
            for (const v of value) {
              if (per.includes(v)) {
                flag = false
              }
            }
            if (!flag) {
              if (!el.parentNode) {
                el.style.display = 'none'
              } else {
                el.parentNode.removeChild(el)
              }
            }
          }
        })
      }
    }
    // 只要包含列出的任意一个权限,元素就会显示
    export const hasAnyPermission = {
      install (Vue) {
        Vue.directive('hasAnyPermission', {
          bind (el, binding, vnode) {
            const permissions = vnode.context.$store.state.user.permissions
            const per = []
            for (const v of permissions) {
              per.push(v.authority)
            }
            const value = binding.value
            let flag = false
            for (const v of value) {
              if (per.includes(v)) {
                flag = true
              }
            }
            if (!flag) {
              if (!el.parentNode) {
                el.style.display = 'none'
              } else {
                el.parentNode.removeChild(el)
              }
            }
          }
        })
      }
    }
    // 必须包含列出的所有角色,元素才显示
    export const hasRole = {
      install (Vue) {
        Vue.directive('hasRole', {
          bind (el, binding, vnode) {
            const permissions = vnode.context.$store.state.user.roles
            const per = []
            for (const v of permissions) {
              per.push(v.authority)
            }
            const value = binding.value
            let flag = true
            for (const v of value) {
              if (!per.includes(v)) {
                flag = false
              }
            }
            if (!flag) {
              if (!el.parentNode) {
                el.style.display = 'none'
              } else {
                el.parentNode.removeChild(el)
              }
            }
          }
        })
      }
    }
    // 只要包含列出的任意一个角色,元素就会显示
    export const hasAnyRole = {
      install (Vue) {
        Vue.directive('hasAnyRole', {
          bind (el, binding, vnode) {
            const permissions = vnode.context.$store.state.user.roles
            const per = []
            for (const v of permissions) {
              per.push(v.authority)
            }
            const value = binding.value
            let flag = false
            for (const v of value) {
              if (per.includes(v)) {
                flag = true
              }
            }
            if (!flag) {
              if (!el.parentNode) {
                el.style.display = 'none'
              } else {
                el.parentNode.removeChild(el)
              }
            }
          }
        })
      }
    }
    复制代码

    在main.js中引入自定义指令

    复制代码
    import Vue from 'vue'
    import { hasPermission, hasNoPermission, hasAnyPermission, hasRole, hasAnyRole } from './utils/permissionDirect'
    
    Vue.use(hasPermission)
    Vue.use(hasNoPermission)
    Vue.use(hasAnyPermission)
    Vue.use(hasRole)
    Vue.use(hasAnyRole)
    复制代码

    这样就可以在按钮中使用自定义指令,没有权限时,按钮自动隐藏,使用Postman工具测试也会拒绝访问

     <a-button type="primary" @click="handleAddUser()" v-hasPermission="['sys:user:add']" icon="plus">新建</a-button>

     后端方法级别权限控制

    @PreAuthorize注解使用需要在SpringSecurity的配置类里添加@EnableGlobalMethodSecurity(prePostEnabled = true)注解,开启基于方法的安全认证机制,也就是说在web层的controller启用注解机制的安全确认,这样就可以使用@PreAuthorize去控制访问方法的权限了

    @Configuration
    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class SecurityConfig extends WebSecurityConfigurerAdapter

    控制层使用方法如下:

    复制代码
    @GetMapping
    @PreAuthorize("hasAuthority('sys:user:view')")
    public Map<String, Object> listUser(QueryRequest queryRequest, UserEntity userEntity) {
    
        return getDataTable(userServiceImpl.findUserDetail(userEntity, queryRequest));
    }
    复制代码

    权限获取

    把权限放在UserDetail的authorities属性中,登录后跟着用户信息传到前端

    复制代码
    private Collection<? extends GrantedAuthority> getUserAuthorities(Long uId) {
            // 用户权限列表,根据用户拥有的权限标识与如 @PreAuthorize("hasAuthority('sys:menu:view')") 标注的接口对比,决定是否可以调用接口
            Set<String> permissions = menuServiceImpl.findUserPermissions(uId).stream().map(MenuEntity::getPerms).collect(Collectors.toSet());
            Collection<? extends GrantedAuthority> authorities = AuthorityUtils.createAuthorityList(permissions.toArray(new String[0]));
            return authorities;
        }
    复制代码

    在UserDetailsService中实现loadUserByUsername方法并设置authorities

    复制代码
    @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            UserEntity u=userServiceImpl.getOne(new QueryWrapper<UserEntity>().eq("FUserName",username));
            if(u!=null) {
                //设置用户角色和权限
                List<RoleEntity> roles= (List<RoleEntity>) roleServiceImpl.listByIds((userRoleServiceImpl.list(new QueryWrapper<UserRoleEntity>().eq("FUserId",u.getId()))).stream().map(UserRoleEntity::getRoleId).collect(Collectors.toList()));
                u.setRoles(roles);
                Collection<? extends GrantedAuthority> authorities = getUserAuthorities(u.getId());
                u.setAuthorities(authorities);
                return u;
            }else {
                throw new AuthenticationCredentialsNotFoundException("当前用户不存在");
            }
        }
    复制代码

     转自:https://www.cnblogs.com/dang-/p/11460698.html

  • 相关阅读:
    所有程序员都是自学成才
    Xcode6模拟器路径
    Xcode7中模拟器的位置
    数据结构中的二级指针和引用
    02_线性表的链式表示和实现
    Spring 单例 httprequest 线程安全
    Springboot listener
    Spring单例 和 Scope注解
    红黑树
    线程池
  • 原文地址:https://www.cnblogs.com/xxl910/p/12720338.html
Copyright © 2020-2023  润新知