思路:
動態路由實現:在導航守衛中判斷用戶是否有用戶信息,通過調用接口,拿到后台根據用戶角色生成的菜單樹,格式化菜單樹結構信息並遞歸生成層級路由表並使用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("當前用戶不存在"); } }