news 2026/10/13 13:07:21

基于Spring Boot+Vue的RBAC权限管理实战:表设计、JWT鉴权与动态路由

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Spring Boot+Vue的RBAC权限管理实战:表设计、JWT鉴权与动态路由

简介:这是一套基于SpringBoot、MyBatis与Vue.js构建的权限管理系统完整源码,适合Java后端及前端开发者学习企业级RBAC权限设计。系统覆盖用户管理、角色管理、角色资源关联、权限分配、日志审计与回收站等核心模块,从数据库表设计到前后端接口联调均有完整实现,可帮助读者快速掌握主流技术栈下的权限控制落地方式。压缩包内含312个文件,以XML映射文件、Java源码、前端页面及配置资源为主,并附带SQL初始化脚本与CDM/PDM数据模型文件,便于理解表结构与业务逻辑。资源整体约292.22MB,文件组织清晰,便于按模块阅读。已有1458人学习下载,适合正在做管理系统毕业设计或希望提升工程能力的开发者用作参考。

1. 为什么几乎每套后台管理系统最后都要补一笔权限:先说结论再拆三件套

做过企业级后台的人应该都有这种经历:业务表建了一堆,管理页面也跑通了,结果验收时业务方突然问“为什么张三能看到财务模块的导出按钮?他应该只能看自己部门的数据”。那一刻你才意识到,权限不是一个功能,而是系统交给运维之前的生死线。这套基于 Spring Boot + MyBatis + Vue 的权限管理系统,就是冲着这个痛点来的,它把「用户→角色→菜单/按钮→数据范围」这条链路的常见实现全部落地,从数据库表设计、登录鉴权、动态菜单到按钮级控制,一步到位。适合正在开发内部管理系统、后台中台项目,或者想把权限模块从零搭一遍的 Java 后端和全栈开发者。接下来我按自己拆过这类资源的顺序,先讲数据模型怎么设计,再进后端和前端的调用链,最后把最容易翻车的位置单列出来。

2. RBAC 五张表落地:用户、角色、权限的数据建模与冗余取舍

2.1 为什么必须用五张表,而不是在用户表里塞一个 role 字段

很多初学者第一步会把权限做成「用户表加一个 role 字段,值写成 admin / user / guest」,这种设计在 Demo 演示里够用,但真实项目第一个月就会被需求击穿。典型场景是:一个用户既要是运营角色能看订单,又要是客服角色能改售后状态,而这两个角色的菜单和按钮权限差异极大,单一的 role 字符串无法表达这种多对多关系,只能被迫加 role2、role3 这种脏列。更致命的是,后续每新增一种角色都要改表结构,权限分配就只能靠改数据库,业务人员根本没法自助操作。

RBAC(Role-Based Access Control,基于角色的访问控制)的核心是引入「角色」作为用户与权限之间的中间层。用户不直接绑定权限,绑定角色,角色再绑定权限集合。这样一来,新增一个「区域经理」角色,只需要在角色表里插入一条记录,再给该角色挂上对应菜单和权限点,不用改用户表,也不用改代码。本资源里的数据库设计正是遵循这个思路,一共拆成五张核心表:

表名作用关键字段
sys_user用户表id, username, password, status, deleted
sys_role角色表id, role_name, role_code, status
sys_menu菜单/权限表id, parent_id, name, perms, type
sys_user_role用户角色关联表user_id, role_id
sys_role_menu角色菜单关联表role_id, menu_id

用户表不直接存角色名,角色表不直接存菜单 JSON,全部通过关联表建立关系。这种设计的直接收益是:查询「某个用户能访问哪些菜单」只需要两次 join,而调整某个角色的权限只需要改 sys_role_menu 里的数据,前端菜单和接口鉴权会同步生效。

2.2 建表 SQL 怎么拆:字段设计里藏着两个容易被忽略的点

这套资源的建表 SQL 基本遵循企业级规范,我挑三个重点字段展开说明。

CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID', username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录账号', password VARCHAR(100) NOT NULL COMMENT 'BCrypt加密后的密码', nickname VARCHAR(50) DEFAULT NULL COMMENT '显示昵称', status TINYINT DEFAULT 1 COMMENT '状态:1启用 0禁用', deleted TINYINT DEFAULT 0 COMMENT '逻辑删除:0未删 1已删', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

密码字段用 BCrypt 而不是 MD5,原因是在权限系统里用户表是攻击者的第一目标,MD5 撞库成本极低,而 BCrypt 每次哈希都带随机盐,相同密码每次存储结果不同,哪怕数据库被脱走,还原明文密码的成本也高得多。status 和 deleted 两个字段含义不同:status 是软启停,账户被禁用时直接改成 0,不用删数据;deleted 是逻辑删除,用户离职后标记为 1,保留历史操作记录和审计线索,避免硬删导致关联表悬空。

sys_menu 表是这套设计的重心,它既要承载左侧菜单树,又要承载按钮权限点:

CREATE TABLE sys_menu ( id BIGINT PRIMARY KEY AUTO_INCREMENT, parent_id BIGINT DEFAULT 0 COMMENT '父菜单ID,0表示根', name VARCHAR(50) NOT NULL COMMENT '菜单/按钮名称', path VARCHAR(200) DEFAULT NULL COMMENT '路由地址,按钮权限为空', component VARCHAR(200) DEFAULT NULL COMMENT '前端组件路径', perms VARCHAR(100) DEFAULT NULL COMMENT '权限标识,如 user:add', type TINYINT NOT NULL COMMENT '类型:1目录 2菜单 3按钮', icon VARCHAR(50) DEFAULT NULL, sort INT DEFAULT 0 COMMENT '排序号', status TINYINT DEFAULT 1 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='菜单权限表';

type 字段是这套表设计的关键。type=1 表示目录,只做分组;type=2 表示可点击跳转的菜单页面;type=3 表示页面内的按钮(比如「新增用户」「删除订单」)。按钮权限点在菜单表里统一管理,好处是权限点可以挂在菜单树下一起做树形展示,前端拿到菜单树后顺带就能得到该用户可用的按钮权限集合,不需要另建表。

2.3 关联表查询的常见写法:一个用户能看到的菜单集合

用户登录后,后端需要一次性查出该用户拥有的菜单、按钮权限全集。SQL 的 join 链路是先走用户角色关联表,再去关联角色菜单表,最后去 sys_menu 里取菜单信息。

SELECT DISTINCT m.*, r.role_code FROM sys_user_role ur INNER JOIN sys_role r ON ur.role_id = r.id AND r.status = 1 INNER JOIN sys_role_menu rm ON r.id = rm.role_id INNER JOIN sys_menu m ON rm.menu_id = m.id AND m.status = 1 WHERE ur.user_id = #{userId} AND r.deleted = 0 ORDER BY m.sort ASC;

这里我刻意加了两个容易漏掉的条件:r.status = 1 和 m.status = 1。如果不加,就会出现角色被禁用但用户仍能看到其菜单的情况。DISTINCT 是因为用户可能同时绑定多个角色,而多个角色又挂了同一个菜单,不加 DISTINCT 会导致前端渲染菜单时出现重复的 key,Vue 列表渲染会直接报警告甚至渲染错乱。ORDER BY sort 保证菜单顺序稳定,前端不需要再做二次排序。

实际项目里一般不会裸调这个 SQL,通常是在 Service 层调用后组装成树形结构返回给前端。组装树的递归逻辑不展开讲,重点要记住:数据库只存平铺的 parent_id,树形组装是后端或者前端的职责,不要把树结构直接存进数据库,后期维护会变成噩梦。

3. Spring Boot + MyBatis 实现认证与鉴权:动态 SQL 与过滤器链

3.1 认证方案选型:为什么这个资源用 JWT 而不是 Session

登录认证有两条主流路线:Session 方案和 JWT 方案。Session 方案需要服务端存储会话状态,集群部署时要引入 Redis 做共享 Session,否则用户在一台机器登录后,请求被负载均衡转发到另一台机器就会掉线。JWT 方案把用户身份信息编码进 token 本身,服务端不存状态,天然适合前后端分离和水平扩展。这套权限管理系统用的是 JWT,典型流程是三段式:

  1. 用户提交用户名密码,后端校验通过后生成 JWT 返回;
  2. 前端把 token 存在本地,每次请求在 HTTP 头里带Authorization: Bearer <token>;
  3. 后端过滤器拦截请求,解析 token、提取用户 ID,然后加载该用户的权限信息。

JWT 在这套资源里的实现结构是登录 Controller → 生成 token → 拦截器解析 token → 把用户信息塞进 ThreadLocal 供后续业务代码使用。核心代码长这样:

@PostMapping("/login") public Result login(@RequestBody LoginDTO dto) { // 1. 先根据用户名查用户,注意要加状态过滤 LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(SysUser::getUsername, dto.getUsername()) .eq(SysUser::getStatus, 1) .eq(SysUser::getDeleted, 0); SysUser user = userMapper.selectOne(wrapper); if (user == null || !BCrypt.checkpw(dto.getPassword(), user.getPassword())) { return Result.error("用户名或密码错误"); } // 2. 生成 token,有效期按项目实际情况定 String token = JwtHelper.createToken(user.getId(), user.getUsername()); return Result.success(token); }

这段代码里最容易忽略的是查用户时同时带 status 和 deleted 两个条件,如果不带 status=1,被禁用的用户依然能登录成功。JwtHelper 内部一般会用 HMAC-SHA256 签名,密钥写在配置文件里,生成 token 时把用户 ID 和过期时间塞进 payload。过期时间建议设置在 4 到 12 小时之间,太短会导致用户频繁重新登录,太长则增大 token 泄露后的风险窗口。

3.2 拦截器里做鉴权:路径匹配与放行名单

有了 token,下一步是写拦截器把未登录请求挡在门外。Spring Boot 里最常用的是实现 HandlerInterceptor 接口,并在 WebMvcConfigurer 里注册。这里有一个很关键的配置:放行名单。登录接口、验证码接口、静态资源路径要放行,其余接口全部要求携带有效 token。

public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 预检请求直接放行 if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { return true; } String authHeader = request.getHeader("Authorization"); if (authHeader == null || !authHeader.startsWith("Bearer ")) { throw new BusinessException("未登录或登录已过期"); } String token = authHeader.substring(7); Long userId = JwtHelper.parseToken(token); // 解析失败会抛异常 UserContext.set(userId); // 存入当前线程上下文 return true; } }

实现拦截器时一个常见的坑是 OPTIONS 预检请求。前端从浏览器发跨域请求时,若后端没配置好跨域响应头,浏览器会先发一个 OPTIONS 请求探路,这个请求不会带 Authorization 头。如果拦截器不单独放行 OPTIONS,前端所有请求都会报「未登录」,而后端日志里根本没有业务异常,排查半天才发现是预检请求被拦了。这段代码里我在最前面判断了 OPTIONS 直接放行,就是这个原因。UserContext 是自定义的 ThreadLocal 容器,请求结束后必须在 afterCompletion 里 remove,否则容器线程池复用时会串用户数据。

3.3 MyBatis 动态 SQL 在权限查询里的三个应用场景

MyBatis 在这套资源里不只是简单的单表 CRUD,权限系统里最复杂的查询是「按角色动态过滤数据」和「菜单树组装」。我挑一个最典型的场景说明:根据用户角色列表动态拼接查询条件,实现不同角色看到不同范围的数据。

<select id="selectUserPage" resultType="com.example.entity.SysUser"> SELECT u.* FROM sys_user u <where> <if test="username != null and username != ''"> AND u.username LIKE CONCAT('%', #{username}, '%') </if> <if test="status != null"> AND u.status = #{status} </if> <if test="roleIdList != null and roleIdList.size() > 0"> AND u.id IN ( SELECT ur.user_id FROM sys_user_role ur WHERE ur.role_id IN <foreach collection="roleIdList" item="roleId" open="(" separator="," close=")"> #{roleId} </foreach> ) </if> </where> </select>

<where>标签会自动处理条件前缀,第一条条件前不会出现多余的 AND。<if>是动态 SQL 的精髓,但要注意 test 里写的是 Java 属性名,不是数据库列名,写错会直接报 OGNL 表达式的错。<foreach>遍历 List 生成 IN 子句时,collection 别名必须和 Mapper 接口方法参数注解一致,比如参数用@Param("roleIdList")标过,这里才能取到。动态 SQL 虽然灵活,但每条 if 分支都要覆盖参数为空的情况,否则容易出现「条件不全导致数据泄露」或者「条件过强导致查不到数据」的极端问题。

按钮权限的后端校验一般是配合 Spring AOP 做注解鉴权,在需要保护的接口上加@PreAuthorize("hasAuthority('user:add')")这类注解,由全局切面统一校验当前用户是否拥有对应权限标识。但这套资源为了降低新手理解成本,更多地采用拦截器加载权限集合后在内存里判断,每个接口在 Service 层入口手动校验。两种方式各有优缺点:注解方式代码侵入小,但权限标识写死在注解里,改动权限点要重新编译发布;代码方式侵入大,但灵活性更高,适合权限点变化频繁的业务系统。

4. Vue 前端把权限焊死在路由上:动态路由与按钮级指令

4.1 动态路由注入:前端从后端拿菜单树再注册路由

前后端分离的权限系统,前端最大的难点是:路由表不能写死。如果写死在代码里,新来的用户也能通过手工输入 URL 跳转到他没有权限的页面。正确的做法是登录成功后,后端返回该用户可访问的菜单树,前端拿到数据后用 Vue Router 的 addRoute 方法动态注册路由。这套资源的前端部分正是这种结构。

// router/index.js const router = new VueRouter({ routes: [ { path: '/login', component: Login }, { path: '/', component: Layout, redirect: '/dashboard' } ] }); // 登录成功后动态添加路由 async function loadAsyncRoutes() { const menuList = await api.getUserMenus(); // 请求后端获取菜单树 const asyncRoutes = generateRoutes(menuList); // 把菜单树转成路由对象 asyncRoutes.forEach(route => { router.addRoute(route); // 逐个注册 }); // 注册一个 404 兜底路由,放在最后 router.addRoute({ path: '*', redirect: '/404', hidden: true }); }

generateRoutes函数的核心是把后端返回的菜单字段映射成 Vue Router 需要的 route 对象:path 对应菜单表的 path,component 需要特殊处理。后端返回的组件路径是字符串,比如system/user/index,前端不能直接用字符串渲染组件,必须通过 require 或者 import 函数动态加载。

function generateRoutes(menus) { const routes = []; menus.forEach(menu => { if (menu.type === 2) { // 只处理菜单类型,目录和按钮不注册路由 const route = { path: menu.path, name: menu.name, component: () => import(`@/views/${menu.component}`) }; if (menu.children && menu.children.length > 0) { route.children = generateRoutes(menu.children); } routes.push(route); } }); return routes; }

这段代码里踩过坑的细节是import(\@/views/${menu.component}`)`。Webpack 打包时,动态 import 的路径必须是能静态分析的字符串,如果写成完全动态的变量拼接,Webpack 无法预知需要打包哪些文件,构建产物里就会缺少对应组件,运行时报「Failed to resolve component」。正确做法是确保菜单表里的 component 字段值和 views 目录下实际路径严格对应,这是前后端约定的一部分,后端在录入菜单数据时就该校验路径是否存在。

4.2 路由守卫与侧边栏同步:刷新页面不再白屏

动态路由注入后面临一个新问题:用户按 F5 刷新页面时,Vue 实例重新创建,路由表被重置成初始状态,此时用户直接访问/system/user,路由表里没有这条记录,页面会白屏。解决方案是配合全局前置守卫做「先拉菜单再放行」的流程。

router.beforeEach(async (to, from, next) => { const token = store.state.token; if (!token && to.path !== '/login') { next('/login'); return; } if (token && !store.state.menusLoaded) { // 首次进入或刷新后:拉菜单、注入路由、继续导航 await store.dispatch('loadMenus'); next({ ...to, replace: true }); return; } next(); });

把菜单注入逻辑放在 beforeEach 里的优点是刷新后自动恢复路由表,用户感知不到路由曾「丢失」。next({ ...to, replace: true })这段是关键技术点:路由表注入完成后,当前导航目标在新路由表中已经存在,但直接 next() 可能还是找不到组件,这里用重导航的方式让 Vue Router 重新解析一次目标路径,才能命中刚注册的动态路由。如果不做这步,刷新到二级页面时经常卡在白屏或一直停在 loading 状态。

侧边栏菜单组件遍历的是 Vuex 里保存的菜单树,和后端返回的数据结构一致,不需要在路由表里再维护一份菜单数据。菜单图标、排序都从后端字段取,前端只做渲染。这套资源里前端状态管理的设计是把菜单树和用户权限集合都放进 Vuex,避免多个组件重复请求后端接口。

4.3 按钮权限指令:v-permission 的实现与适用范围

菜单权限解决的是「能不能看到这个页面」,按钮权限解决的是「能不能点这个按钮」。比如「删除用户」按钮,普通管理员登录后就不应该出现在页面上。这套资源用自定义指令实现按钮权限控制。

// directives/permission.js Vue.directive('permission', { inserted(el, binding) { const requiredPerms = binding.value; // 比如 ['user:delete'] const userPerms = store.state.permissions; // 当前用户拥有的权限点集合 const hasPermission = requiredPerms.some(p => userPerms.includes(p)); if (!hasPermission) { el.parentNode && el.parentNode.removeChild(el); } } });

使用方式是在按钮上挂指令:<el-button v-permission="['user:delete']">删除</el-button>。指令生效时机是元素插入 DOM 后立刻执行,如果没有权限直接移除该按钮。这个方案直接直观,但有一个边界问题要刻意强调:前端按钮隐藏只是用户体验层面的优化,不是安全措施。真正保护按钮对应接口的必须是后端鉴权,否则懂技术的人完全可以绕过前端直接发 HTTP 请求调用删除接口。资源和多数正规项目的约定是「前后端双校验」,前端控制展示,后端控制执行,两者缺一不可。

按钮权限集合是从登录时后端返回的 menu/perms 列表里提取出来的。后端返回的每条菜单记录里,type=3 的记录其 perms 字段就是按钮权限标识,前端一次性存到 Vuex,指令判断从 store 里取值,这样就避开了刷新后权限集合丢失的问题。

5. 权限系统避坑实录:五条踩过的坑与处理方案

5.1 超级管理员硬编码角色 ID,导致后续扩展被锁死

遇到过的情况是资源把超级管理员判断写成if (roleId == 1),数据库里手动埋了一条 id=1 的角色当作超管。这个写法在单项目里能用,但一旦做多租户或者需要复制一套环境,id 对不上就全乱了,而且删掉初始化脚本里的第一条数据后,所有管理员全部失去超管权限,排查半天毫无头绪。

原因:超管的判断逻辑绑定在了具体数据上,而不是绑定在业务性质上。解决:用户表加is_super字段,或者角色表加role_code = 'SUPER_ADMIN'作为唯一标识,判断时用编码不用自增 ID。从那以后我再写权限系统,超管判断一律走 role_code 查数据字典,不再依赖任何具体 ID。

5.2 动态路由刷新后白屏,团队以为浏览器缓存问题

有段时间前端同事频繁反馈「用户刷新就白屏,清缓存又能好一阵」。一开始怀疑是浏览器缓存了旧版本路由,后来用无痕模式复现,才发现是动态路由注入时机的问题。刷新时 Vue 实例重建,路由表只剩登录页那条初始路由,用户直接打开深层页面路径,路由匹配不到任何组件,渲染结果就是空白。

原因:动态路由只在登录后注入一次,刷新不持久化。解决:按 4.2 节的做法在全局守卫里判断 menusLoaded 状态,刷新后先重新拉取菜单并注入路由,再重放导航目标。同时给 Vuex 的 menusLoaded 加持久化到 sessionStorage 的辅助逻辑,避免每次刷新都调一次后端接口,减少不必要的等待。

5.3 MyBatis 嵌套菜单树查询报 NPE,根因是映射关系没配对

组装菜单树时,原资源里用的是 MyBatis 的 collection 嵌套查询,运行时一直报NullPointerException,日志定位到 menu 的 children 属性为 null。检查 resultMap 发现子查询的类型和外部 SQL 的列名对不上,查询返回了空 List 但 MyBatis 没自动初始化 children 字段。

原因:MyBatis 的 collection 标签需要显式声明javaType和ofType,并且外层查询结果列名必须和子查询关联列一致,否则映射不上。解决:用<collection property="children" ofType="Menu" select="selectChildrenByParentId" column="{id=id}"/>明确指定关联列,或者更稳妥的做法是查全部菜单后在 Java 内存里手动组装树,数据量不大时这种方案反而更好排错,也便于加缓存。

5.4 权限变更后旧 token 里的权限不更新,用户不退出就一直用旧权限

出现过这样一种情况:管理员把某个用户的角色从「运营」降为「访客」,结果该用户登录状态没退出,刷新后菜单还是运营那套,接口也能继续调。问题根因是登录时只把用户 ID 写进 token,权限集合是登录后每次请求再从数据库查询的,但前端菜单是登录时一次性返回的,后端改了角色,前端菜单没更新。

原因:token 里不存权限本身,但前端缓存了旧菜单,后端新增了权限但前端拉取菜单的时机不对。解决:在用户角色变更后,让后端把该用户的 JWT 版本号标识改掉,比如在 token 里放一个perm_version字段,角色变更时自增,前端拦截器收到 401 后强制重新登录,这是最粗暴直接的方案;或者改为每次路由守卫触发时都调一次接口校验菜单版本,版本不同才重新拉取。后一种方案体感更好,但要保证接口响应速度。

5.5 按钮权限只做了前端隐藏,接口没加防护被直接调用数据泄露

有次测试拿到一个「删除订单」的按钮,前端被隐藏了,但用工具直接发 DELETE 请求到后端接口,返回 200,数据被删了。前端权限指令的移除只是改 DOM,后端接口没有对应的鉴权逻辑,攻击者根本不需要经过前端页面。

原因:前端隐藏和真正的权限控制是两码事,后端没有在 Service 或 Controller 层做权限点校验。解决:确定每个按钮对应的接口路径,在拦截器或者 AOP 切面里做权限标识校验,明确「前端隐藏」和「后端拒绝」是并列关系,缺一个都不算权限完成。从那以后我每做完一个页面,都会打开接口文档把所有按钮对应的 URL 过一遍,确认每个 URL 都挂了权限校验。

6. 再往前走一步:数据权限、菜单缓存与越权测试

权限系统做完了菜单和按钮,距离真正可用还差最后一块:数据权限。菜单权限控制「能看到哪个页面」,按钮权限控制「能点哪个功能」,那「能看哪些数据」怎么控制?常见思路是给角色加一个数据范围字段,比如 user 类型只能看自己创建的数据,dept 类型只能看本部门数据,all 类型看全部数据。资源里可以扩展一张角色表字段,在业务查询时通过 AOP 切面自动拼接 SQL 条件。

实现时我一般会在 Service 层定义一个上下文工具类,从用户信息中取出数据范围,然后在查询 SQL 的动态条件里加一层 where 过滤。比如查询订单列表时,如果是部门数据范围,就自动追加AND dept_id = #{currentUserDeptId}。这个逻辑必须放在后端,前端只负责把已过滤的数据渲染出来,否则就是裸奔。

另一个值得做的是菜单缓存。每次用户刷新页面都要查一遍菜单树,数据库压力不小,尤其菜单表数据量和用户量都上来以后。我通常用 Redis 缓存每个用户的菜单树,key 设计为menu:tree:{userId},用户的角色权限变更时删除对应 key,超管修改菜单表时按角色清除所有相关缓存。但要注意一个坑:如果系统里有租户概念,key 里还要拼上租户 ID,否则跨租户数据会串。缓存的失效时机比缓存本身更重要,宁可失效早也不要失效晚,权限变更后 30 秒内让旧菜单失效,避免用户觉得系统「卡了」而反复刷新确认。

越权测试是最容易被跳过的步骤,也是最体现权限系统质量的一环。我自己的习惯是每次上线前准备两套账号:一个只有查看权限,一个有完全权限。测试清单固定如下:普通用户直接访问未授权 URL;已登录用户修改自己的角色 ID 后重放请求;两个用户互相猜对方资源 ID 去访问;退出登录后使用旧 token 访问接口。这四类操作全部不允许出现 200 响应,一旦出现就说明后端存在越权漏洞。

这套权限管理系统的价值不在于代码量有多庞大,而在于它把「用户-角色-权限-数据范围」这条链路完整串了起来。从数据库字段设计到 JWT 拦截器,再到前端动态路由和按钮指令,每一个环节都有对应的坑位和取舍。当初我接手类似项目时,花了整整两天排查「刷新白屏」和「权限变更不生效」两个问题,事后才意识到这是动态路由和缓存失期没做好的通病。从那以后,我再写权限模块时都会强制自己走一遍完整的越权测试清单,把前端隐藏、后端校验、数据过滤三者分开验证。希望这篇文章能帮你省下那两天排查时间,把精力花在真正的业务逻辑上。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 13:07:17

大模型本地持续进化:Sidecar+LoRA后训练实战解析

1. 项目缘起与核心思路&#xff1a;为什么“本地AI”需要持续进化“让大模型在本地持续进化”听起来像是把训练跑在了每个人电脑上&#xff0c;但其实完全不是那么回事。我一开始接触这个项目时&#xff0c;以为又是某种分布式训练框架&#xff0c;后来仔细看完设计才发现&…

作者头像 李华
网站建设 2026/10/13 13:07:15

用Qt实现INI配置文件编辑器:保留注释、顺序与编码兼容

简介&#xff1a;一份面向Qt开发者的通用ini配置文件编辑器&#xff0c;提供Qt源码与可直接运行的可执行程序。项目基于QSettings类完成ini文件的打开、读写与保存&#xff0c;支持字符串、整数等各类键值类型&#xff1b;通过setValue、value、toString、toInt等API处理写入、…

作者头像 李华
网站建设 2026/10/13 13:06:48

GOCI2波段表解读与海洋水色遥感反演实战:SIF计算与赤潮监测

简介&#xff1a;第二代地球同步轨道海洋水色成像仪GOCI2说明手册&#xff0c;集中整理其波段信息与关键性能指标&#xff0c;面向海洋遥感数据处理、水色研究及传感器定标领域的科研人员和工程师&#xff0c;有助于快速弄清GOCI至GOCI2的升级差异与观测能力。资源包内含1个PDF…

作者头像 李华
网站建设 2026/10/13 13:02:04

Mac上Qt SQL驱动加载失败的完整排查与修复指南

先交代一下背景&#xff1a;某次我把项目迁移到 Mac 上重新编译&#xff0c;跑起来之后去连数据库&#xff0c;控制台直接给我甩了三行 “QSqlDatabase: QMYSQL driver not loaded”&#xff0c;再往下追一行 “available drivers: QSQLITE”。那会儿我还没意识到&#xff0c;这…

作者头像 李华
网站建设 2026/10/13 13:01:53

RAG 细节与优化理解

RAG 细节与优化理解 本文是 RAG 系列第二篇。第一篇已经介绍了文档处理、分块、Query、召回和重排的完整流程&#xff0c;本文不再重复基础概念&#xff0c;重点理解 Bi-Encoder、Cross-Encoder 的工作方式&#xff0c;以及如何定位和优化 RAG 检索效果。 一、RAG 优化的基本思…

作者头像 李华