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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM