本文是依据工作中对权限办理的理解,在 [RuoYi-Vue] (gitee.com/y_project/R…) 的基础上依照自己的主意完结权限办理体系的核心逻辑。
权限办理模型
权限和权限规划
在一个页面上,权限指的是页面是否能拜访,菜单栏是否显现,按钮是否可点击。后端接口上,权限指的是api是否可调用,数据是否可悉数/部分或许不可获取。总的来说能够将权限规划为两大类:功用权限和数据权限:
- 功用权限:指的是页面,菜单栏,按钮(api的调用)这类能够被操作的权限
- 数据权限:指的是数据的拜访范围。比如在一张表中记载一切职工的数据,数据依据区域范围进行划分,A区域的办理员只能拜访A区域的职工数据,B区域的办理员只能拜访B区域的职工数据。在这个比如中区域就是维度,A区域和B区域就是维度值。
权限办理
现在干流的权限办理模型都是依据RBAC,关于RBAC不了解的能够自行搜索,或许看之前谈过的 【项目实践】后台办理体系前后端实践一:权限操控原理 。
结合RBAC和权限规划,一个简略的权限模型规划如下:
用户相关人物(多对多),人物相关功用权限和数据权限(多对多),功用权限相关页面,菜单和按钮(多对duo),数据权限相关维度,依据不同的维度值进行数据增删改查操作(多对多)。
功用全限完结
依照这个简略的权限模型,完结的效果图如下:
admin 具有一切的权限,test 只具有人物办理和权限办理页面的部分权限:
test 相关的功用权限为common 对应的为限,资源列表查询接口未授权,所以无法得到数据。
资源装备
功用权限包括页面,菜单和按钮(API拜访),这里称为资源。
资源表规划如下:
关于页面和菜单增改操作,点击按钮激活弹窗进行修正调用接口将设置后的数据保存到数据库,删除则直接调用接口。
在资源图标挑选的时分有个细节问题,资源图标挑选是一个下拉框,会显现一切的可用图标,完结办法是运用 require.context
经过转化后得到图标文件名。但是 require.context
是 webpack专有的,假如运用 vite 则不可需要改为 import.meta.globEager()
按钮(API)的权限则是经过调用资源同步接口,该接口会获取后端一切的接口,并将接口数据保存在数据库中。代码完结如下:
/**
* 同步api信息
*
* @param request 恳求
* @return {@link ResultVO}<{@link String}>
*/
@GetMapping("/api/sync")
@PreAuthorize("@ss.hasPermission('resource:api:sync')")
@ApiOperation(value = "Api资源同步", notes = "同步api信息")
public ResultVO<String> syncApiInfo(HttpServletRequest request){
List<SysResource> sysResourceList = resourceService.syncApiInfo(request);
resourceService.addApiList(sysResourceList);
return ResultVO.success("同步成功");
}
/**
* 增加api列表
*
* @param sysResourceList 体系资源列表
*/
@Override
public void addApiList(List<SysResource> sysResourceList) {
resourceMapper.addApiList(sysResourceList);
}
/**
* 得到一切的api信息
*
* @param request 恳求
* @return {@link List}<{@link SysResource}>
*/
@Override
public List<SysResource> syncApiInfo(HttpServletRequest request) {
ServletContext servletContext = request.getSession().getServletContext();
List<SysResource> sysApiList = new ArrayList<>();
if (servletContext != null) {
WebApplicationContext appContext = WebApplicationContextUtils.getWebApplicationContext(servletContext);
assert appContext != null;
// 获取一切的 RequestMapping
Map<String, HandlerMapping> allRequestMappings = BeanFactoryUtils.beansOfTypeIncludingAncestors(appContext, HandlerMapping.class, true, false);
allRequestMappings.forEach((name, handlerMapping) -> {
if (handlerMapping instanceof RequestMappingHandlerMapping) {
RequestMappingHandlerMapping requestMappingHandlerMapping = (RequestMappingHandlerMapping) handlerMapping;
Map<RequestMappingInfo, HandlerMethod> handlerMethodsMap = requestMappingHandlerMapping.getHandlerMethods();
handlerMethodsMap.forEach((requestMappingInfo, handlerMethods) -> {
SysResource sysResource = new SysResource();
// 获取 controller 的 class 的全类名
Class<?> handlerMethodsBeanType = handlerMethods.getBeanType();
// 获取 control 内的办法
Method method = handlerMethods.getMethod();
// 获取恳求办法
RequestMethodsRequestCondition requestMethodsRequestCondition = requestMappingInfo.getMethodsCondition();
sysResource.setRequestType(set2String(requestMethodsRequestCondition.getMethods()));
// 获取恳求地址
PatternsRequestCondition patternsCondition = requestMappingInfo.getPatternsCondition();
sysResource.setPath(set2String(patternsCondition.getPatterns()));
// 获取办法名
sysResource.setMethodName(method.getName());
// 获取类名
String className = handlerMethodsBeanType.toString().replace("class", "")
.replace(" ", "");
sysResource.setControllerClass(className);
if (handlerMethodsBeanType.isAnnotationPresent(Api.class)) {
Api apiOperation = handlerMethodsBeanType.getAnnotation(Api.class);
String[] tags = apiOperation.tags();
sysResource.setControllerName(String.join(",", tags));
}
if (method.isAnnotationPresent(PreAuthorize.class)) {
PreAuthorize preAuthorize = method.getAnnotation(PreAuthorize.class);
String preAuthorizeVal = preAuthorize.value();
sysResource.setPerms(getPermissionStr(preAuthorizeVal));
}
if (method.isAnnotationPresent(ApiOperation.class)) {
ApiOperation apiOperation = method.getAnnotation(ApiOperation.class);
String value = apiOperation.value();
sysResource.setResourceName(value);
String notes = apiOperation.notes();
sysResource.setDescription(StringUtils.isNull(notes) ? value : notes);
}
if (StringUtils.isNotNull(sysResource.getResourceName())) {
sysApiList.add(sysResource);
}
});
}
});
}
return sysApiList;
}
履行完接口同步后,其实并不知道各个接口和各菜单之间的关系,这时能够经过功用权限关系快速修正窗口进行拖拽。
功用权限装备
功用权限是将页面,菜单,按钮权限进行组合,权限字符串是装备好的功用权限的仅有标识。功用权限和资源经过关系表进行相关。
点击修正按钮能够重新装备功用权限,此时得到的功用权限列表为按钮和菜单相相关的权限树
人物规划
人物表规划中,考虑到查询用户具有的权限标识的需求,权限标识是在resource表,连表查询需要查找7张表。增加 function_json
字段保存的是该人物的相关的功用权限的id,来下降连表查询。保存 role_function 的关系表能够经过权限反查人物。
人物规划现在仅仅将人物和功用权限进行相关
人物修正和新增也是修正和功用权限的相关性
终究的表结构如下:
权限操控完结
页面和菜单操控
后端部分
关于页面和菜单的操控,完结办法是依据当时用户查询相关的人物,依据人物的 function_json
字段 function_resource_relation 和 resource 连表查询取得用户能拜访的资源。
/**
* 获取菜单列表,type = "M"和 "C"
*
* @return {@link List}<{@link ResourceVo}>
*/
@GetMapping("/menu")
@ApiOperation(value = "菜单资源查询",notes = "获取菜单列表")
public ResultVO<List<ResourceVo>> getMenuList() {
UserDetail user = AuthenticationContextHolder.getCurrentUser();
List<SysResource> sysResourceList = resourceService.getMenuList(user.getSysUser());
List<ResourceVo> resourceVoList = new ArrayList<>();
SysResource.pos2vos(sysResourceList, resourceVoList);
return ResultVO.success(resourceVoList);
}
/**
* 取得菜单列表
* @param sysUser sysUser
*/
@Override
public List<SysResource> getMenuList(SysUser sysUser) {
List<SysResource> sysResourceList = getUserResource(sysUser);
return list2Tree(sysResourceList);
}
/**
* 经过functionId列表得到资源列表
*
*
* @param sysUser@return {@link List}<{@link SysResource}>
*/
@Override
public List<SysResource> getUserResource(SysUser sysUser) {
List<SysRole> roleList = sysUser.getRoleList();
Set<Long> functionIds = new HashSet<>();
roleList.forEach(sysRole -> {
List<Long> functionIdList = JSONArray.parseArray(sysRole.getFunctionJson(), Long.class);
functionIds.addAll(functionIdList);
});
List<SysResource> resourceList = new ArrayList<>();
if (functionIds.size() > 0) {
resourceList = resourceMapper.getResourceListByFunctions(new ArrayList<>(functionIds));
}
return resourceList;
}
/**
* 将菜单列表转化为菜单树
*
* @param sysResourceList 体系菜单列表
* @return {@link List}<{@link SysResource}>
*/
private List<SysResource> list2Tree(List<SysResource> sysResourceList) {
List<SysResource> sysResourceResult = new ArrayList<>();
if (sysResourceList != null) {
// 获取 parentId = 0的根节点
sysResourceResult = sysResourceList.stream().filter(sysMenu -> sysMenu.getParentId().equals(0L)).collect(Collectors.toList());
// 依据 parentId 进行分组
Map<Long, List<SysResource>> map = sysResourceList.stream().collect(Collectors.groupingBy(SysResource::getParentId));
recursionTree(sysResourceResult, map);
}
return sysResourceResult;
}
/**
* 生成递归树
*
* @param menuList 树列表
* @param map 目标
*/
private void recursionTree(List<SysResource> menuList, Map<Long, List<SysResource>> map) {
menuList.forEach(tree -> {
List<SysResource> childList = map.get(tree.getResourceId());
tree.setChildren(childList);
if (tree.getChildren() != null && tree.getChildren().size() > 0) {
recursionTree(childList, map);
}
});
}
前端部分
前端部分主要分为以下步骤:
- 用户登录后,调用后端接口获取资源列表
- 得到资源列表后,只拿资源类型为 M(目录) 和 C(菜单),其实也能够后端完结。同时生成路由表即将资源列表由树型结构平铺。
- 经过
router.addRoute
办法将生成的路由表加入到 routes 中 - 依据资源列表运用递归组件生成侧边栏
router.beforeEach(async (to, from, next) => {
const { usePermissionState, generateMenusAction } = appStore.permissionStore;
const { userState, setUserInfoAction } = appStore.userStore;
NProgress.start();
if (getToken()) {
// 登陆后token没过期,路由地址是登陆页直接跳转到主页
if (to.path === "/login") {
next({ path: '/' });
} else {
// 登陆后,直接放行
// 设置当时用户的信息,包括姓名,头像,人物,权限信息
await setUserInfoAction();
// 设置当时用户的左边菜单
await generateMenusAction(userState.permissions);
// 依据菜单栏生成路由
generateRoutes(usePermissionState.rolesRoutes);
// 处理运用动态路由地址直接拜访,或许刷新页面导致无法找到路由的问题 No match found
if (to.path == '/404' && to.redirectedFrom != undefined) {
if (router.getRoutes().find(item => item.path === to.redirectedFrom?.path)) {
next({ path: to.redirectedFrom?.fullPath, replace: true })
} else {
next('/notFound')
}
} else {
next()
}
}
} else {
// 登陆后token过期,路由地址是白名单直接放行
if (whiteList.includes(to.path)) {
next();
// next({ path: to.path, query: { redirect: to.fullPath } });
} else {
// 登陆后token过期,跳转到主页,query 放入当时路由的path
if (to.path == '/404' && to.redirectedFrom != undefined) {
next({ path: "/login", query: { redirect: to.redirectedFrom?.fullPath } });
} else {
next({ path: "/login", query: { redirect: to.fullPath } });
}
}
}
});
function generateRoutes(menusPath: Resource[]) {
menusPath.length > 0 && menusPath.forEach(menu => {
router.addRoute("index", {
path: `/${menu.path}`,
name: menu.path,
// component: () => import(`@/views/${menu.component}.vue`)
component:
//需要用vite规定的导入办法导入,否则打包后部署到服务器报错找不到动态导入的文件,
//对应上方的const modules = import.meta.glob("../views/**/**.vue")
//运用/* @vite-ignore */则不会在开发是报错
modules[/* @vite-ignore */`../views/${menu.component}.vue`],
})
})
}
按钮(api)权限操控
关于按钮(api)的权限操控,大致能够分为三个步骤:
- 用户登录认证经往后,查询用户的权限标识放入到 redis 中,并将 token 回来给用户
- 用户拜访接口时,带上 token ,token 验证经过从 redis 中取出权限标识
- 拜访接口时,经过
@PreAuthorize("@ss.hasPermission('permission:role:query')")
注解判别是否有权限
/**
* 获取用户权限
*
* @param sysUser 体系用户
* @return {@link List}<{@link String}>
*/
public List<String> getUserPermissionById(SysUser sysUser) {
Set<String> userPermission = new HashSet<>();
if (sysUser.getUserId().equals(1L)) {
userPermission.add(ALL_PERMISSION);
} else {
List<SysResource> resourceList = resourceService.getUserResource(sysUser);
resourceList.forEach(sysResource -> {
if (StringUtils.isNotNull(sysResource.getPerms())) {
userPermission.add(sysResource.getPerms());
}
});
}
return new ArrayList<>(userPermission);
}
/**
* 经过functionId列表得到资源列表
*
*
* @param sysUser@return {@link List}<{@link SysResource}>
*/
@Override
public List<SysResource> getUserResource(SysUser sysUser) {
List<SysRole> roleList = sysUser.getRoleList();
Set<Long> functionIds = new HashSet<>();
roleList.forEach(sysRole -> {
List<Long> functionIdList = JSONArray.parseArray(sysRole.getFunctionJson(), Long.class);
functionIds.addAll(functionIdList);
});
List<SysResource> resourceList = new ArrayList<>();
if (functionIds.size() > 0) {
resourceList = resourceMapper.getResourceListByFunctions(new ArrayList<>(functionIds));
}
return resourceList;
}
/**
* 是否有权限
*
* @param permission 权限
* @return boolean
*/
public boolean hasPermission(String permission) {
if (StringUtils.isEmpty(permission)) {
throw new AuthException(ResultCodeEnum.UNACCESS.getCode(), ResultCodeEnum.UNACCESS.getMessage());
}
UserDetail user = AuthenticationContextHolder.getCurrentUser();
log.info("PermissionService ---> hasPermission:{}", user);
if (user.getPermissions().contains(ALL_PERMISSION)) {
return true;
}
if (!user.getPermissions().contains(permission)) {
throw new AuthException(ResultCodeEnum.UNACCESS.getCode(), ResultCodeEnum.UNACCESS.getMessage());
}
return true;
}
终究,关于数据权限部分现在查询到的处理方案利用AOP抓取到用户对应人物的一切数据规则并进行SQL拼接,终究在SQL层面完结数据过滤,后续参阅 数据权限这样完结,yyds 来完结,别的前端页面部分出现还有部分待完善。
源码链接:vue3-auth (gitee.com) java-auth (gitee.com)