ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Hadess权限控制实战:从后端接口鉴权到前端按钮权限的全链路方案

Hadess权限控制实战:从后端接口鉴权到前端按钮权限的全链路方案 权限这块很多项目做到最后都是“能登录就行”一旦被问“这个按钮为什么A角色能看见、B角色看不见”就开始乱了。我这些年见过太多项目在权限上翻车要么后端接口裸奔要么前端按钮全靠v-if写死一堆判断改一次需求要动十几个文件。Hadess这套权限控制方案属于把后端鉴权和前端按钮权限整体打通的做法今天我把整个落地的思路、代码、还有踩过的坑一次说完。1. 权限控制的整体设计思路从大而全到小而细1.1 先想清楚你要控制到什么粒度权限控制最忌讳一上来就写代码。先问自己一个问题你这个系统需要控制到哪一层我用过一个很直观的分法接口权限这个角色能不能调用某个后端API属于第一道防线。菜单权限登录后能看到哪些导航项决定了用户的操作范围。按钮权限页面里某个按钮比如“删除”、“审核”、“导出”能不能显示和点击这是最细的颗粒度。数据权限同一个接口不同角色返回的数据范围不同比如销售只能看自己的订单主管能看全组的。这一般是最后做的也最容易踩坑。Hadess这套方案的定位是覆盖到“按钮权限”这一层。它不是只做后端拦截而是把权限标识贯穿后端接口和前端页面元素形成一条完整的链路用户在页面上能看见什么、能点动什么后端就允许他做什么。这样权限才不会只是一个摆设。1.2 为什么按钮权限是最大痛点做过企业级应用的都知道菜单权限用现成框架做起来不难后端返回一个菜单树前端按路由渲染就行。真正麻烦的是按钮。很多系统的按钮权限是靠前端手写的if (user.role admin || user.role boss) { // 显示删除按钮 }这种写法短期能用角色一多就完蛋。今天加一个“运营专员”明天加一个“区域经理”每加一个角色就要把所有if条件重新捋一遍。更可怕的是前端只是藏了按钮懂技术的人直接绕过页面调后端接口照样能把数据删了。所以Hadess的思路是前端控制“看不看得见”后端控制“能不能执行”两者必须用同一套权限标识。1.3 RBAC模型是这一切的地基不管是Hadess还是别的权限框架底层都离不开RBAC基于角色的访问控制。这个模型理解起来很朴素把“权限”这个不好管理的东西拆成三个实体——用户、角色、权限。用户和角色是多对多角色和权限是多对多。用户不直接关联权限而是通过角色间接获得权限。举个例子“张三”是“运营专员”角色“运营专员”拥有“order:export”导出订单权限那么张三就能导出订单。如果明天不让运营专员导出了只需要从角色上摘掉这一个权限标识所有这个角色的人立刻生效不用挨个改用户。这就是RBAC的核心价值一套权限标识管全网。2. 核心细节解析权限模型设计是成败关键2.1 权限标识规范一句话说清“谁能做什么”Hadess对权限标识的约定是格式化的字符串推荐用冒号分层模块、功能、动作。比如order:view查看订单order:create创建订单order:update编辑订单order:delete删除订单order:export导出订单user:resetPwd重置密码这样一套标识后端在接口上声明前端在按钮上声明两边完全一致。冒号分层的最大好处是可以用通配符。比如给角色分配order:*就等于把订单模块下的所有操作权限一起给了不用一个个勾选。我在实际项目里还会再加一个横杠后缀来表示数据范围比如order:delete:own只能删自己的订单让标识本身自带业务语义。2.2 用户、角色、权限三张表怎么设计直接贴我常用的表结构-- 用户表 CREATE TABLE sys_user ( id BIGINT PRIMARY KEY, username VARCHAR(50) NOT NULL, password VARCHAR(100) NOT NULL, status TINYINT DEFAULT 1, create_time DATETIME ); -- 角色表 CREATE TABLE sys_role ( id BIGINT PRIMARY KEY, role_code VARCHAR(50) NOT NULL, role_name VARCHAR(50) NOT NULL ); -- 权限表 CREATE TABLE sys_permission ( id BIGINT PRIMARY KEY, perm_code VARCHAR(100) NOT NULL, perm_name VARCHAR(100) NOT NULL, module VARCHAR(50) ); -- 用户-角色关联表 CREATE TABLE sys_user_role ( user_id BIGINT NOT NULL, role_id BIGINT NOT NULL, PRIMARY KEY (user_id, role_id) ); -- 角色-权限关联表 CREATE TABLE sys_role_permission ( role_id BIGINT NOT NULL, permission_id BIGINT NOT NULL, PRIMARY KEY (role_id, permission_id) );很多教程只列这三张表但我建议实际项目里一定要加一张sys_permission的“父级标识”字段比如按钮权限属于哪个菜单权限否则改权限的时候很难看清层级关系。菜单和按钮其实可以放在同一张权限表里用类型字段区分1代表菜单2代表按钮。这样一次查询就把菜单树和按钮标识全部拿到省一次往返。2.3 认证与授权两件事不能混为一谈认证Authentication解决的是“你是谁”的问题。Hadess通过登录接口校验用户名密码签发一个Token。后面每次请求都带上TokenHadess根据Token找到当前用户。授权Authorization解决的是“你能干什么”的问题。Hadess拿到当前用户后去查他有哪些角色、哪些权限标识再判断他能不能访问当前接口。很多项目出问题就是因为把这两个阶段混在一起。认证通过不等于授权通过。我见过有的系统只要登录成功所有接口都放行内部系统还能忍但凡对外提供服务就属于裸奔。Hadess的底层是把两个阶段拆开处理的登录只负责把身份确认了访问每个接口时重新做一次授权校验。3. 实操全流程在Hadess里把权限控制完整跑通3.1 环境准备实操之前先把基础环境准备好。Hadess依赖Spring Boot 2.5以上版本JDK建议直接用1.8或者11数据库我用MySQL 5.7。引入核心依赖dependency groupIdcom.hadess/groupId artifactIdhadess-spring-boot-starter/artifactId version2.1.0/version /dependency配置application.ymlhadess: auth: token-expire: 7200 # Token有效期秒 header-name: Authorization ignored-urls: - /api/login - /api/captcha permission: enable: true # 开启权限校验 super-admin: admin # 超级管理员角色跳过权限校验ignored-urls这一步极其关键。如果你把登录接口也纳入权限校验那就变成先有鸡还是先有蛋的问题了——还没登录哪来的Token所以登录、验证码、健康检查这些接口必须放行。超级管理员角色也要单独配置本质上它不走普通权限逻辑属于系统保留角色。3.2 登录接口签发Token并加载权限登录逻辑不算复杂但有一个细节要重点处理用户登录成功后除了生成Token还要把用户拥有的权限标识列表查出来一起返回给前端。前端后续的按钮显隐全靠这份权限列表。PostMapping(/api/login) public ResultVO login(RequestBody LoginDTO dto) { // 1. 校验账号密码 SysUser user userService.checkLogin(dto.getUsername(), dto.getPassword()); // 2. 生成Token String token hadessAuthManager.generateToken(user.getId(), user.getUsername()); // 3. 查询权限标识列表返回给前端 SetString permissions permissionService.getPermissionsByUserId(user.getId()); // 4. 组装返回 return ResultVO.success(new LoginVO(token, permissions)); }这里我再多说一句权限列表建议一次查完直接放Rediskey的格式类似permissions:userId有效期和Token对齐。以后判断接口权限时先从Redis取取不到再查数据库。这一招能让接口响应时间下降一大截。3.3 后端接口做权限声明注解方式最直观后端权限控制的实现方式我个人最推荐自定义注解。它最大的价值是把权限声明和业务逻辑解耦在方法上写一行注释注解业务代码里完全不用出现权限判断的代码逻辑干净又统一。Target({ElementType.METHOD}) Retention(RetentionPolicy.RUNTIME) public interface RequiresPermission { String value(); // 权限标识 }控制器里的用法RestController RequestMapping(/api/order) public class OrderController { GetMapping(/list) RequiresPermission(order:view) public ResultVO list() { return ResultVO.success(orderService.list()); } PostMapping(/delete) RequiresPermission(order:delete) public ResultVO delete(RequestBody Long id) { orderService.delete(id); return ResultVO.success(); } }拦截器统一处理Component public class PermissionInterceptor extends HandlerInterceptorAdapter { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod (HandlerMethod) handler; RequiresPermission annotation handlerMethod.getMethodAnnotation(RequiresPermission.class); if (annotation ! null) { // 核心校验逻辑 String requiredPermission annotation.value(); SetString userPermissions getCurrentUserPermissions(); if (!userPermissions.contains(requiredPermission)) { throw new PermissionDeniedException(no permission); } } } return true; } }这个实现的妙处在于方法上没有加注解的接口默认放行。所以新写的代码如果一时忘了加权限注解至少不会误伤正常功能。但这也意味要形成团队规矩新增接口必须写上对应权限标识这个可以靠代码评审把关。3.4 前端按钮权限vue项目里的两大实现套路终于说到最新热词里问得最多的vue按钮权限怎么控制。前端按钮权限拿到后端返回的权限标识列表后普遍有两种做法。第一种自定义指令v-permission。个人强烈推荐理由是不污染页面里的业务逻辑模板里一眼就能看出按钮需要什么权限// 在入口处注册全局指令 Vue.directive(permission, { inserted(el, binding) { // 从Vuex中拿到当前用户权限列表 const permissions store.state.user.permissions; const required binding.value; if (!permissions.some(p p required)) { el.parentNode el.parentNode.removeChild(el); } } });页面中使用template div el-button v-permissionorder:delete删除/el-button el-button v-permissionorder:export导出/el-button /div /template这里有个坑我必须提醒inserted钩子执行的时候元素已经渲染完毕此时移除元素会有闪烁。秒杀场景下权限页面会闪一下。解决方式是改用beforeMount或者在渲染前拦截更彻底的做法是在拿到权限数据之后就过滤按钮的显示状态。第二种用v-if方法判断。适合权限逻辑比较复杂的场景比如“有导出权限且当前选中数据超过10条”才显示导出按钮el-button v-ifcanShowExportBtn导出/el-buttoncomputed: { canShowExportBtn() { return this.$store.state.user.permissions.includes(order:export) this.selectedRows.length 10; } }这种写法的优点是灵活缺点是代码量大了以后computed里会堆很多类似的判断。我的习惯是纯权限判断一律用指令带业务条件的权限判断用v-ifcomputed。两种方式结合页面代码既干净又灵活。3.5 动态菜单和路由按钮权限做完了还得顺带把菜单做掉。否则按钮控制得再细菜单一样“他不该看的全看得到”体验上就漏了。Hadess推荐的方案是登录后根据权限过滤菜单树再用router.addRoutes动态挂载路由// 根据后端菜单数据生成路由 const buildRoutes (menus) { const routes []; menus.forEach(menu { const route { path: menu.path, name: menu.name, component: () import(/views/${menu.component}), children: menu.children ? buildRoutes(menu.children) : [] }; routes.push(route); }); return routes; }; // 拿到菜单后挂载 router.addRoutes(buildRoutes(menuTree));动态路由有一点烦就是刷新页面时路由会丢失。因为store里的数据是内存中的一刷新就没了。解决办法是刷新时先调一次“获取用户信息”接口重新拉取菜单和权限再重新挂载。这个顺序必须固定先获取权限再挂路由否则页面会渲染成空白。3.6 数据权限接口里怎么控制数据范围按钮权限解决“能不能操作”数据权限解决“能操作哪些数据”。这一层虽然Hadess也有内置支持但业务差异大我建议先理解清楚再决定用不用框架自带的。最轻量级的方式是把数据范围条件拼进查询参数里。比如Hadess内置了一个数据权限解析器可以在查询前自动追加条件GetMapping(/list) RequiresPermission(order:view) DataScope(type DataScopeType.OWN) public ResultVO list() { return ResultVO.success(orderService.list()); }OWN代表只能看自己的数据后端会往查询的条件里自动拼一个WHERE create_by 当前用户ID。这个方案在中小项目里足够用。数据权限设计起来比功能权限繁琐得多不仅有“自己的/本部门的/全部”这种维度还有父子部门关系、兼职多部门等情况建议先从最简单的维度做起最好不要一开始就设计出需要动态拼接各种逻辑的条件。4. 常见问题与排查技巧我踩过的权限坑4.1 按钮权限不生效前端页面还在报错最典型的现象权限标识配了按钮还是看不到控制台报v-permission is not defined。原因基本是这两种自定义指令注册晚了组件已经渲染完了还没全局注册或者指令里的store.state.user.permissions还没初始化比如异步请求还没返回就已经被读取了。我的排查顺序是固定的先看登录返回的permissions里有没有这个权限标识后端查一下角色权限关联表确认不是数据库没配到位。再看指令有没有全局注册注册代码是不是在Vue实例创建之前执行的。最后看指令里拿权限数据有没有做空保护如果permissions是空数组includes一定为false按钮必然被移除这个现象会伪装成“权限配置不对”。4.2 改了角色权限按钮状态不更新这个问题几乎每个用RBAC的项目都会遇到。管理员在后台给某角色加了个“导出”权限那个角色的用户刷新页面后还是看不到导出按钮。为什么因为前端权限数据是登录时一次性塞进store的后端改了权限前端内存里的数据不会自动同步。排查路径看后端有没有把权限数据做缓存。如果Redis里存了permissions:userId管理员改权限时必须同步删除缓存或者引入版本号让它自动失效。看登录重新获取的机制。最稳妥的做法是提供“切换账号”或“刷新权限”的功能不要逼用户退出再登录。我习惯在处理权限变更的后台接口里主动调用一次缓存清理把受影响的用户权限缓存删掉。给个简单的缓存清理代码示例public void updateRolePermissions(Long roleId, ListLong permissionIds) { rolePermissionService.updateByRoleId(roleId, permissionIds); // 删除该角色下所有用户的权限缓存 ListLong userIds userRoleService.getUserIdsByRoleId(roleId); userIds.forEach(uid - redisHelper.delete(permissions: uid)); }4.3 接口权限和按钮权限不一致按钮明明不带删除权限要求但用户却能删掉数据。这类问题的根源往往是前端和后端各写了一套权限标识前端按钮上声明的是order:delete后端接口校验的是order:del两边压根对不上号。治理这个问题的办法是统一权限标识的字典表。把权限码当成一种资源来管理前端组件里引用权限码时不能手写字符串尽量从常量文件里引用// perms.js export const PERMS { ORDER_DELETE: order:delete, ORDER_EXPORT: order:export };页面使用el-button v-permissionPERMS.ORDER_DELETE删除/el-button后端接口注解里的字符串也直接从权限表里复制。这样前后端引用同源从机制上杜绝了不一致。4.4 超级管理员也被权限拦截有次我帮同事排查问题发现admin账号配置了超级管理员角色但访问接口还是被拦截。研究一圈发现问题出在角色判断的代码写错位置了。拦截器里要判断当前用户角色是否为admin而不是判断商品里有没有某个权限Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod (HandlerMethod) handler; RequiresPermission annotation handlerMethod.getMethodAnnotation(RequiresPermission.class); if (annotation ! null) { String requiredPermission annotation.value(); // 先判断是否超级管理员是则直接放行 if (isSuperAdmin()) { return true; } SetString userPermissions getCurrentUserPermissions(); if (!userPermissions.contains(requiredPermission)) { throw new PermissionDeniedException(no permission); } } } return true; }注意顺序先判断管理员再判断权限。否则管理员不管有没有权限都会被拦或者更糟管理员也走普通权限判断一旦漏配置了某个权限标识反而导致管理员无权限。4.5 前端按钮权限的几大误区误区一权限全部藏在按钮显示上接口不做二次校验。这是安全意识的问题前端隐藏只是体验手段后端校验才是安全保证。误区二权限标识写得让人看不懂比如p_1、p_2这种。三个月后再改代码没人知道p_1是删除还是导出。宁可长一点也要语义化。误区三所有按钮都做权限控制。有些按钮比如“刷新”、“展开”这种通用操作完全不需要控制。权限标识多了后期维护成本也跟着上来。误区四动态路由菜单和权限变更时机对不上。权限改了菜单没跟着刷新结果导航里的入口和页面实际可访问范围不一致也是待排查的坑。4.6 排查工具推荐快速定位权限问题我通常按三个层次来查浏览器开发者工具看登录接口返回的permissions列表、看发起的请求头里带了什么Token。后端日志拦截器拦截到的请求路径、校验的权限码、当前用户拥有的权限列表强烈建议在权限校验失败时打一条日志包括用户ID、请求路径、需要的权限。SQL排查直接查用户-角色-权限的三张关系表确认关联数据没丢。很多时候“有权限但校验不通过”就是角色关联表里数据被误删了。权限校验失败时后端日志一定不能只写“No Permission”要么带上用户ID、要么带上权限码。否则用户找你说“我没权限了”你又没法复现光靠猜真的能猜一天。5. 权限控制扩展思路为了权限这块设计了一些附加注意事项关于权限控制很多人都容易陷进一个误区觉得权限控制就是后端的事情。实际上做全链路权限控制时前端才是最先感知到变化的那一层。有一句实际开发的话我非常认同权限要像呼吸一样自然平时感觉不到它的存在一旦被拦下来就该有清晰的提示和引导。我给团队定的规范是“权限三件套”配置前先想清楚权限标识的命名语义开发时前端指令与后端注解同步写上线后每两周抽查一次角色和权限的映射关系是否跟业务变动一致。这样可以防止因为组织架构调整之后权限表变成一锅粥。另外权限控制的日志审计不要省。谁在什么时候调用了删除接口、导出过哪份数据、被哪个权限拦截过这些最好都记录下来。一方面出现越权问题时溯源方便另一方面也是规范化管理的基本要求。如果项目接下来要接入第三方登录比如企业微信、钉钉扫码登录要把第三方用户的身份映射到系统内的用户表和角色表权限逻辑本身不用变变的只是认证入口。这也是Hadess把认证和授权拆开的好处换认证方式授权模型和权限标识体系几乎可以不动。数据库里权限码的维护我还习惯每月导一次全量权限表和代码里的注解做对照。实现方式可以写个小脚本扫描后端方法上的注解和前端的权限码常量输出一份差异清单。权限码漂移这种事大部分系统都在犯越早建立检查机制越省心。最后再分享一个从实战里得出的经验权限控制的复杂度往往不是技术带来的而是业务规则带来的。角色数量一多、授权维度一多各种奇怪需求就来了什么“A角色能看到B部门的订单但不能导出”这种需求其实最适合把权限规则做进权限码里而不是在代码里写分支判断。Blessing业务规则的代码越来越多时重构权限模型周期就到了。从我个人的实操体会来说按钮级别的权限控制最好用的状态是前端不需要知道用户是什么角色只需要知道他有没有某个权限标识界面优雅逻辑清爽。Hadess把后端权限校验和前端按钮控制统一到这套标识体系里面权限码在设计时下得了功夫后面开发维护会省非常多的事。控制在手里越简单越可靠。
返回列表