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

时间:2021-11-28 18:09:56

思路:

动态路由实现:在导航守卫中判断用户是否有用户信息,通过调用接口,拿到后台根据用户角色生成的菜单树,格式化菜单树结构信息并递归生成层级路由表并使用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("当前用户不存在");
}
}