☰
Hadess权限控制实战:从后端接口鉴权到前端按钮权限的全链路方案
2026/9/28 5:21:52 网站建设 项目流程

权限这块,很多项目做到最后都是“能登录就行”,一旦被问“这个按钮为什么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。后面每次请求都带上Token,Hadess根据Token找到当前用户。
  • 授权(Authorization)解决的是“你能干什么”的问题。Hadess拿到当前用户后,去查他有哪些角色、哪些权限标识,再判断他能不能访问当前接口。

很多项目出问题就是因为把这两个阶段混在一起。认证通过不等于授权通过。我见过有的系统只要登录成功,所有接口都放行,内部系统还能忍,但凡对外提供服务就属于裸奔。Hadess的底层是把两个阶段拆开处理的,登录只负责把身份确认了,访问每个接口时重新做一次授权校验。

3. 实操全流程:在Hadess里把权限控制完整跑通

3.1 环境准备

实操之前先把基础环境准备好。Hadess依赖Spring Boot 2.5以上版本,JDK建议直接用1.8或者11,数据库我用MySQL 5.7。引入核心依赖:

<dependency> <groupId>com.hadess</groupId> <artifactId>hadess-spring-boot-starter</artifactId> <version>2.1.0</version> </dependency>

配置application.yml:

hadess: 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. 查询权限标识列表,返回给前端 Set<String> permissions = permissionService.getPermissionsByUserId(user.getId()); // 4. 组装返回 return ResultVO.success(new LoginVO(token, permissions)); }

这里我再多说一句,权限列表建议一次查完直接放Redis,key的格式类似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(); Set<String> 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-permission="'order:delete'">删除</el-button> <el-button v-permission="'order:export'">导出</el-button> </div> </template>

这里有个坑我必须提醒:inserted钩子执行的时候元素已经渲染完毕,此时移除元素会有闪烁。秒杀场景下权限页面会闪一下。解决方式是改用beforeMount或者在渲染前拦截,更彻底的做法是在拿到权限数据之后就过滤按钮的显示状态。

第二种,用v-if方法判断。适合权限逻辑比较复杂的场景,比如“有导出权限,且当前选中数据超过10条”才显示导出按钮:

<el-button v-if="canShowExportBtn">导出</el-button>
computed: { canShowExportBtn() { return this.$store.state.user.permissions.includes('order:export') && this.selectedRows.length > 10; } }

这种写法的优点是灵活,缺点是代码量大了以后,computed里会堆很多类似的判断。我的习惯是:纯权限判断一律用指令,带业务条件的权限判断用v-if+computed。两种方式结合,页面代码既干净又灵活。

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, List<Long> permissionIds) { rolePermissionService.updateByRoleId(roleId, permissionIds); // 删除该角色下所有用户的权限缓存 List<Long> 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-permission="PERMS.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; } Set<String> 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把后端权限校验和前端按钮控制统一到这套标识体系里面,权限码在设计时下得了功夫,后面开发维护会省非常多的事。控制在手里,越简单越可靠。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询