vue基于d2-admin的RBAC权限管理解决方案

前两篇关于vue权限路由文章的填坑,说了一堆理论,是时候操作一波了。vue权限路由实现方式总结vue权限路由实现方式总结二选择d2-admin是因为element-ui的相关开源项目里,d2-admin的结构和代码是让我感到最舒服的,而且基于d2-admin实现RBAC权限管理也很方便,对d2-admin没有大的侵入性的改动。预览地址Github 相关概念不了解RBAC,可以看这里企业管理系统前后端分离架构设计 系列一 权限模型篇实现了RBAC模型权限控制菜单与路由独立管理,完全由后端返回user存储用户admin标识用户是否为系统管理员role存储角色信息roleUser存储用户与角色的关联关系menu存储菜单信息,类型分为菜单与功能,一个菜单下可以有多个功能,菜单类型的permission字段标识访问这个菜单需要的功能权限,功能类型的permission字段相当于此功能的别称,所以菜单类型的permission字段为其某个功能类型子节点的permission值permission存储角色与功能的关联关系interface存储接口信息functionInterface存储功能与接口关联关系,通过查找用户所属角色,再查找相关角色所具备的功能权限,再通过相关功能就可以查出用户所能访问的接口route存储前端路由信息,通过permission字段过滤出用户所能访问的路由运行流程及相关API使用d2admin的原有登录逻辑,全局路由守卫中判断是否已经拉取权限信息,获取后标识为已获取。const token = util.cookies.get(’token’) if (token && token !== ‘undefined’) { //拉取权限信息 if (!isFetchPermissionInfo) { await fetchPermissionInfo(); isFetchPermissionInfo = true; next(to.path, true) } else { next() } } else { // 将当前预计打开的页面完整地址临时存储 登录后继续跳转 // 这个 cookie(redirect) 会在登录后自动删除 util.cookies.set(‘redirect’, to.fullPath) // 没有登录的时候跳转到登录界面 next({ name: ’login’ }) }//标记是否已经拉取权限信息let isFetchPermissionInfo = falselet fetchPermissionInfo = async () => { //处理动态添加的路由 const formatRoutes = function (routes) { routes.forEach(route => { route.component = routerMapComponents[route.component] if (route.children) { formatRoutes(route.children) } }) } try { let userPermissionInfo = await userService.getUserPermissionInfo() permissionMenu = userPermissionInfo.accessMenus permissionRouter = userPermissionInfo.accessRoutes permission.functions = userPermissionInfo.userPermissions permission.roles = userPermissionInfo.userRoles permission.interfaces = util.formatInterfaces(userPermissionInfo.accessInterfaces) permission.isAdmin = userPermissionInfo.isAdmin == 1 } catch (ex) { console.log(ex) } formatRoutes(permissionRouter) let allMenuAside = […menuAside, …permissionMenu] let allMenuHeader = […menuHeader, …permissionMenu] //动态添加路由 router.addRoutes(permissionRouter); // 处理路由 得到每一级的路由设置 store.commit(‘d2admin/page/init’, […frameInRoutes, …permissionRouter]) // 设置顶栏菜单 store.commit(‘d2admin/menu/headerSet’, allMenuHeader) // 设置侧边栏菜单 store.commit(‘d2admin/menu/fullAsideSet’, allMenuAside) // 初始化菜单搜索功能 store.commit(‘d2admin/search/init’, allMenuHeader) // 设置权限信息 store.commit(‘d2admin/permission/set’, permission) // 加载上次退出时的多页列表 store.dispatch(‘d2admin/page/openedLoad’) await Promise.resolve()}后端需要返回的权限信息包括权限过滤后的角色编码集合,功能编码集合,接口信息集合,菜单列表,路由列表,以及是否系统管理员标识。格式如下{ “statusCode”: 200, “msg”: “”, “data”: { “userName”: “MenuManager”, “userRoles”: [ “R_MENUADMIN” ], “userPermissions”: [ “p_menu_view”, “p_menu_edit”, “p_menu_menu” ], “accessMenus”: [ { “title”: “系统”, “path”: “/system”, “icon”: “cogs”, “children”: [ { “title”: “系统设置”, “icon”: “cogs”, “children”: [ { “title”: “菜单管理”, “path”: “/system/menu”, “icon”: “th-list” } ] }, { “title”: “组织架构”, “icon”: “pie-chart”, “children”: [ { “title”: “部门管理”, “icon”: “html5” }, { “title”: “职位管理”, “icon”: “opencart” } ] } ] } ], “accessRoutes”: [ { “name”: “System”, “path”: “/system”, “component”: “layoutHeaderAside”, “componentPath”: “layout/header-aside/layout”, “meta”: { “title”: “系统设置”, “cache”: true }, “children”: [ { “name”: “MenuPage”, “path”: “/system/menu”, “component”: “menu”, “componentPath”: “pages/sys/menu/index”, “meta”: { “title”: “菜单管理”, “cache”: true } }, { “name”: “RoutePage”, “path”: “/system/route”, “component”: “route”, “componentPath”: “pages/sys/route/index”, “meta”: { “title”: “路由管理”, “cache”: true } }, { “name”: “RolePage”, “path”: “/system/role”, “component”: “role”, “componentPath”: “pages/sys/role/index”, “meta”: { “title”: “角色管理”, “cache”: true } }, { “name”: “UserPage”, “path”: “/system/user”, “component”: “user”, “componentPath”: “pages/sys/user/index”, “meta”: { “title”: “用户管理”, “cache”: true } }, { “name”: “InterfacePage”, “path”: “/system/interface”, “component”: “interface”, “meta”: { “title”: “接口管理” } } ] } ], “accessInterfaces”: [ { “path”: “/menu/:id”, “method”: “get” }, { “path”: “/menu”, “method”: “get” }, { “path”: “/menu/save”, “method”: “post” }, { “path”: “/interface/paged”, “method”: “get” } ], “isAdmin”: 0, “avatarUrl”: “https://api.adorable.io/avatars/85/abott@adorable.png" }}设置菜单将固定菜单(/menu/header、/menu/aside)与后端返回的权限菜单(accessMenus)合并后,存入相应的vuex store模块中…let allMenuAside = […menuAside, …permissionMenu]let allMenuHeader = […menuHeader, …permissionMenu]…// 设置顶栏菜单store.commit(‘d2admin/menu/headerSet’, allMenuHeader)// 设置侧边栏菜单store.commit(‘d2admin/menu/fullAsideSet’, allMenuAside)// 初始化菜单搜索功能store.commit(‘d2admin/search/init’, allMenuHeader)处理路由默认使用routerMapComponents 的方式处理后端返回的权限路由//处理动态添加的路由const formatRoutes = function (routes) { routes.forEach(route => { route.component = routerMapComponents[route.component] if (route.children) { formatRoutes(route.children) } })}…formatRoutes(permissionRouter)//动态添加路由router.addRoutes(permissionRouter);// 处理路由 得到每一级的路由设置store.commit(‘d2admin/page/init’, […frameInRoutes, …permissionRouter])路由处理方式及区别可看vue权限路由实现方式总结二设置权限信息将角色编码集合,功能编码集合,接口信息集合,以及是否系统管理员标识存入相应的vuex store模块中…permission.functions = userPermissionInfo.userPermissionspermission.roles = userPermissionInfo.userRolespermission.interfaces = util.formatInterfaces(userPermissionInfo.accessInterfaces)permission.isAdmin = userPermissionInfo.isAdmin == 1…// 设置权限信息store.commit(‘d2admin/permission/set’, permission)接口权限控制以及loading配置支持使用角色编码,功能编码以及接口权限进行控制,如下export function getMenuList() { return request({ url: ‘/menu’, method: ‘get’, interfaceCheck: true, permission:[“p_menu_view”], loading: { type: ’loading’, options: { fullscreen: true, lock: true, text: ‘加载中…’, spinner: ’el-icon-loading’, background: ‘rgba(0, 0, 0, 0.8)’ } }, success: { type: ‘message’, options: { message: ‘加载菜单成功’, type: ‘success’ } } })}interfaceCheck: true表示使用接口权限进行控制,如果vuex store中存储的接口信息与当前要请求的接口想匹配,则可发起请求,否则请求将被拦截。permission:[“p_menu_view”]表示使用角色编码和功能编码进行权限校验,如果vuex store中存储的角色编码或功能编码与当前表示的编码相匹配,则可发起请求,否则请求将被拦截。源码位置在libs/permission.js,可根据自己需求进行修改loading配置相关源码在libs/loading.js,根据自己需求进行配置,success也是如此,源码在libs/loading.js。 照此思路可以自行配置其它功能,比如请求失败等。页面元素权限控制使用指令v-permission: <el-button v-permission:function.all=”[‘p_menu_edit’]" type=“primary” icon=“el-icon-edit” size=“mini” @click=“batchEdit” >批量编辑</el-button>参数可为function、role,表明以功能编码或角色编码进行校验,为空则使用两者进行校验。修饰符all,表示必须全部匹配指令值中所有的编码。源码位置在plugin/permission/index.js,根据自己实际需求进行修改。使用v-if+全局方法:<el-button v-if=“canAdd” type=“primary” icon=“el-icon-circle-plus-outline” size=“mini” @click=“add” >添加</el-button>data() { return { canAdd: this.hasPermissions([“p_menu_edit”]) }; },默认同时使用角色编码与功能编码进行校验,有一项匹配即可。类似的方法还要hasFunctions,hasRoles。源码位置在plugin/permission/index.js,根据自己实际需求进行修改。不要使用v-if=“hasPermissions([‘p_menu_edit’])“这种方式,会导致方法多次执行也可以直接在组件中从vuex store读取权限信息进行校验。开发建议页面级别的组件放到pages/目录下,并且在routerMapCompnonents/index.js中以key-value的形式导出不需要权限控制的固定菜单放到menu/aside.js和menu/header.js中不需要权限控制的路由放到router/routes.js frameIn内需要权限控制的菜单与路由通过界面的管理功能进行添加,确保菜单的path与路由的path相对应,路由的name与页面组件的name一致才能使keep-alive生效,路由的component在routerMapCompnonents/index.js中能通过key匹配到。开发阶段菜单与路由的添加可由开发人员自行维护,并维护一份清单,上线后将清单交给相关的人去维护即可。如果觉得麻烦,不想菜单与路由由后端返回,可以在前端维护一份菜单和路由(路由中的component还是使用字符串,参考mock/permissionMenuAndRouter.js),并且在菜单和路由上面维护相应的权限编码,一般都是使用功能编码。后端就不需要返回菜单和路由信息了,但是其他权限信息,比如角色编码,功能编码等还是需要的。通过后端返回的功能编码列表,在前端过滤出用户具备权限的菜单和路由,过滤处理后后的菜单与路由格式与之前由后端返回的格式一致,然后将处理后的菜单与路由当做后端返回的一样处理即可。数据mock与代码生成数据mock使用lazy-mock修改而来的d2-admin-server,数据真实来源于后端,相比其他工具,支持数据持久化,存储使用的是json文件,不需要安装数据库。简单的配置即可自动生成增删改查的接口。后端使用中间件控制访问权限,比如: .get(’/menu’, PermissionCheck(), controllers.menu.getMenuList)PermissionCheck默认使用接口进行校验,校验用户所能访问的API中是否匹配当前API,支持使用功能编码与角色编码进行校验PermissionCheck([“p_menu_edit”],[“r_menu_admin”],true),第一个参数为功能编码,第二个为角色编码,第三个为是否使用接口进行校验。更多详细用法可看lazy-mock文档前端代码生成还在开发中… ...

January 6, 2019 · 3 min · jiezi

spring boot 利用注解实现权限验证

这里使用 aop 来实现权限验证引入依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId></dependency>定义注解package com.lmxdawn.api.admin.annotation;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;/** * 后台登录授权/权限验证的注解 ///此注解只能修饰方法@Target(ElementType.METHOD)//当前注解如何去保持@Retention(RetentionPolicy.RUNTIME)public @interface AuthRuleAnnotation { String value();}拦截实现登录和权限验证package com.lmxdawn.api.admin.aspect;import com.lmxdawn.api.admin.annotation.AuthRuleAnnotation;import com.lmxdawn.api.admin.enums.ResultEnum;import com.lmxdawn.api.admin.exception.JsonException;import com.lmxdawn.api.admin.service.auth.AuthLoginService;import com.lmxdawn.api.common.utils.JwtUtils;import io.jsonwebtoken.Claims;import lombok.extern.slf4j.Slf4j;import org.aspectj.lang.JoinPoint;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.aspectj.lang.annotation.Pointcut;import org.aspectj.lang.reflect.MethodSignature;import org.springframework.stereotype.Component;import org.springframework.web.context.request.RequestContextHolder;import org.springframework.web.context.request.ServletRequestAttributes;import javax.annotation.Resource;import javax.servlet.http.HttpServletRequest;import java.lang.reflect.Method;import java.util.List;/* * 登录验证 AOP /@Aspect@Component@Slf4jpublic class AuthorizeAspect { @Resource private AuthLoginService authLoginService; @Pointcut("@annotation(com.lmxdawn.api.admin.annotation.AuthRuleAnnotation)") public void adminLoginVerify() { } /* * 登录验证 * * @param joinPoint / @Before(“adminLoginVerify()”) public void doAdminAuthVerify(JoinPoint joinPoint) { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attributes == null) { throw new JsonException(ResultEnum.NOT_NETWORK); } HttpServletRequest request = attributes.getRequest(); String id = request.getHeader(“X-Adminid”); Long adminId = Long.valueOf(id); String token = request.getHeader(“X-Token”); if (token == null) { throw new JsonException(ResultEnum.LOGIN_VERIFY_FALL); } // 验证 token Claims claims = JwtUtils.parse(token); if (claims == null) { throw new JsonException(ResultEnum.LOGIN_VERIFY_FALL); } Long jwtAdminId = Long.valueOf(claims.get(“admin_id”).toString()); if (adminId.compareTo(jwtAdminId) != 0) { throw new JsonException(ResultEnum.LOGIN_VERIFY_FALL); } // 判断是否进行权限验证 MethodSignature signature = (MethodSignature) joinPoint.getSignature(); //从切面中获取当前方法 Method method = signature.getMethod(); //得到了方,提取出他的注解 AuthRuleAnnotation action = method.getAnnotation(AuthRuleAnnotation.class); // 进行权限验证 authRuleVerify(action.value(), adminId); } /* * 权限验证 * * @param authRule / private void authRuleVerify(String authRule, Long adminId) { if (authRule != null && authRule.length() > 0) { List<String> authRules = authLoginService.listRuleByAdminId(adminId); // admin 为最高权限 for (String item : authRules) { if (item.equals(“admin”) || item.equals(authRule)) { return; } } throw new JsonException(ResultEnum.AUTH_FAILED); } }}Controller 中使用使用 AuthRuleAnnotation 注解, value 值就是在数据库里面定义的 权限规则名称/* * 获取管理员列表 */@AuthRuleAnnotation(“admin/auth/admin/index”)@GetMapping("/admin/auth/admin/index")public ResultVO index(@Valid AuthAdminQueryForm authAdminQueryForm, BindingResult bindingResult) { if (bindingResult.hasErrors()) { return ResultVOUtils.error(ResultEnum.PARAM_VERIFY_FALL, bindingResult.getFieldError().getDefaultMessage()); } if (authAdminQueryForm.getRoleId() != null) { List<AuthRoleAdmin> authRoleAdmins = authRoleAdminService.listByRoleId(authAdminQueryForm.getRoleId()); List<Long> ids = new ArrayList<>(); if (authRoleAdmins != null && !authRoleAdmins.isEmpty()) { ids = authRoleAdmins.stream().map(AuthRoleAdmin::getAdminId).collect(Collectors.toList()); } authAdminQueryForm.setIds(ids); } List<AuthAdmin> authAdminList = authAdminService.listAdminPage(authAdminQueryForm); // 查询所有的权限 List<Long> adminIds = authAdminList.stream().map(AuthAdmin::getId).collect(Collectors.toList()); List<AuthRoleAdmin> authRoleAdminList = authRoleAdminService.listByAdminIdIn(adminIds); // 视图列表 List<AuthAdminVo> authAdminVoList = authAdminList.stream().map(item -> { AuthAdminVo authAdminVo = new AuthAdminVo(); BeanUtils.copyProperties(item, authAdminVo); List<Long> roles = authRoleAdminList.stream() .filter(authRoleAdmin -> authAdminVo.getId().equals(authRoleAdmin.getAdminId())) .map(AuthRoleAdmin::getRoleId) .collect(Collectors.toList()); authAdminVo.setRoles(roles); return authAdminVo; }).collect(Collectors.toList()); PageInfo<AuthAdmin> authAdminPageInfo = new PageInfo<>(authAdminList); PageSimpleVO<AuthAdminVo> authAdminPageSimpleVO = new PageSimpleVO<>(); authAdminPageSimpleVO.setTotal(authAdminPageInfo.getTotal()); authAdminPageSimpleVO.setList(authAdminVoList); return ResultVOUtils.success(authAdminPageSimpleVO);}相关地址GitHub 地址: https://github.com/lmxdawn/vu… ...

November 24, 2018 · 2 min · jiezi

项目文档说明:react + Ant Design 的 blog-react-admin

前言此 blog-react-admin 项目是基于 蚂蚁金服开源的 ant design pro 之上,用 react 全家桶 + Ant Design 的进行再次开发的,项目已经开源,项目地址在 github 上。效果预览 https://preview.pro.ant.design/user/login1. 后台管理1.1 已经实现功能[x] 登录[x] 文章管理(支持 MarkDown 语法)[x] 标签管理[x] 留言管理[x] 用户管理[x] 友情链接管理[x] 时间轴管理1.2 待实现功能[ ] 点赞、留言和评论 的通知管理[ ] 评论管理[ ] 个人中心(用来设置博主的各种信息)[ ] 工作台( 接入百度统计接口,查看网站浏览量和用户访问等数据 )2. 主要项目结构- pages - Account 博主个人中心 - article 文章管理 - Category 分类 - Dashboard 工作台 - Exection 403 404 500 等页面 - Link 链接管理 - Message 留言管理 - OtherUser 用户管理 - Tag 标签管理 - TimeAsix 时间轴 - User 登录注册管理文章管理、用户管理、留言等 具体业务需求,都是些常用的逻辑可以实现的,也很简单,这里就不展开讲了。3. 使用使用详情请查看 Ant Design Pro ,因为本项目也是在这个基础之上,按这个规范来构建的。4. 缺点开发时,程序出错后,修改正确后,webpack 有时不会及时查觉到内容已经更改,从而不能及时编译,要重新运行命令打包。5. 项目地址开源不易,如果觉得该项目不错或者对你有所帮助,欢迎到 github 上给个 star,谢谢。项目地址:前台展示: https://github.com/乐趣区/blog-react管理后台:https://github.com/乐趣区/blog-react-admin后端:https://github.com/乐趣区/blog-nodeblog:https://github.com/乐趣区/blog本博客系统的系列文章:react + node + express + ant + mongodb 的简洁兼时尚的博客网站react + Ant Design + 支持 markdown 的 blog-react 项目文档说明[基于 node + express + mongodb 的 blog-node 项目文档说明] 敬请期待…6. Build Setup ( 构建安装 )# install dependenciesnpm install # serve with hot reload at localhost: 3000npm start # build for production with minificationnpm run build 如果要看完整的效果,是要和后台项目 blog-node 一起运行才行的,不然接口请求会失败。7. 最后对 全栈开发 有兴趣的朋友可以扫下方二维码关注我的公众号,我会不定期更新有价值的内容。微信公众号:乐趣区分享 前端、后端开发等相关的技术文章,热点资源,全栈程序员的成长之路。关注公众号并回复 福利 便免费送你视频资源,绝对干货。福利详情请点击: 免费资源分享–Python、Java、Linux、Go、node、vue、react、javaScript ...

November 23, 2018 · 1 min · jiezi