资讯动态

Spring Boot+MyBatis+Vue.js构建RBAC权限管理系统实战

发布时间:2026/10/14 0:46:53 来源:尧图企业网站定制
简介面向Java全栈开发者与后端初学者这份基于SpringBootMyBatisVue.js的权限管理系统完整呈现了现代后台权限体系的开发思路。系统覆盖用户管理、角色管理、角色资源管理、角色权限管理与日志管理并引入回收站机制用户管理涉及账号创建、禁用与重置角色权限可精确到CRUD操作适合理解RBAC模型和通用后台模块设计。资源包共312个文件主要包含java源码、xml映射、class字节码、jar依赖、sql脚本、yml配置及构建文件压缩后约292.22MB可作为课设、毕设或企业权限模块的参考蓝本。目前已有1458人学习下载。从后端分层结构、MyBatis SQL映射到前端Vue.js界面交互都能获得可运行的代码与配置素材便于二次开发和排错。1. 权限管理系统用什么做才不翻车先看清这套组合在解决什么问题很多开发者第一次接触权限管理系统是从给自己的后台加一个“登录拦截”开始的。用户表里存一个角色字段登录时判断一下页面里用v-if控制某个按钮显示不显示。这套写法在 Demo 阶段跑得通但一旦业务角色超过三个、菜单权限开始分部门分数据范围代码就会变成一团乱麻每个接口里复制粘贴权限判断、前端路由写死、新来的同事不敢动那几百行 if else。这个时候再去了解基于 Spring Boot MyBatis Vue.js 的权限管理系统才会明白它解决的从来不是“能不能登录”而是“谁在什么条件下能访问哪些接口、看到哪些菜单、操作哪些按钮”这一整套可控、可维护、可扩展的授权模型。这篇文章会把这套组合的落地路径拆开讲从表设计到后端鉴权再到前端路由收敛最后把那些最容易让人返工的坑点列清楚。2. 为什么是RBAC JWT权限模型、会话方案与这套技术栈的选型理由2.1 RBAC模型是权限系统的地基先别急着写代码权限管理系统里最常说的就是 RBAC全称 Role-Based Access Control基于角色的访问控制。它的核心思想是不直接把权限分配给用户而是把权限分配给角色再把角色分配给用户。用户和权限之间隔着一层角色好处是当几十个用户需要同一个权限时只需要改角色不需要逐个改用户。常见做法是五张核心表用户表sys_user、角色表sys_role、菜单/权限表sys_menu、用户角色关联表sys_user_role、角色菜单关联表sys_role_menu。有些团队会单独拆出一张sys_permission表存按钮权限但我个人建议菜单表里用类型字段区分目录、菜单、按钮三类避免表数量膨胀。菜单表的层级结构用父子 ID 表达这也是 Element UI 菜单组件最常见的输入格式。这套模型的优势在授权粒度上体现得很明显。后端接口用PreAuthorize(hasAuthority(system:user:add))这类注解做方法级拦截前端用路由守卫控制页面可见性用自定义指令控制按钮显隐三个层级各管一段互相之间通过权限标识字符串关联。某个用户能看见用户管理页面但点不了“新增”按钮这种需求在 RBAC 里就是一条角色菜单关联数据的事。2.2 会话方案选JWT还是Session要看你的部署形态权限系统必然要回答一个问题用户登录之后服务端怎么识别他是谁。传统方案是 Session登录后把用户信息存在服务端内存或 Redis 里给浏览器一个 Session ID。优点是服务端可以随时吊销会话缺点是集群部署时要处理 Session 同步要么用 Spring Session 做 Redis 共享要么用粘滞会话把请求固定到同一台机器。JWTJSON Web Token是另一种常见选择。登录成功后服务端签发一个带签名和过期时间的 Token 返回给前端前端存在本地存储里每次请求放在 Authorization 头里带上。服务端通过验签拿到用户 ID 和角色信息不需要在服务端保存会话状态天然支持水平扩展。用这套 Spring Boot Vue.js 前后端分离的组合时我一般会选 JWT。原因是前后端分离后静态资源可能部署在 CDN 或单独的 Nginx 上后端只做 APISession 方案在这种形态下要额外处理跨域时携带 Cookie 的问题而 JWT 只需要前端在请求头里塞一个字符串。要注意的是 JWT 有两个常见的坑一是 Token 无法主动吊销用户改密码或被盗号踢下线会比较麻烦需要引入 Token 黑名单或缩短过期时间加刷新机制二是不要把太多敏感信息放进 PayloadJWT 的 Payload 只是 Base64 编码没有加密任何人都能解出来看。2.3 选MyBatis而不是JPA不是情怀是可控性这个标题里指定了 MyBatis不是没有道理的。权限管理系统的查询场景非常偏运维和运营用户列表要按部门过滤、按关键字搜索、按时间范围筛选角色列表要带出已分配的菜单 ID 集合菜单表要递归查子节点。这类 SQL 通常多表关联偶尔还要写子查询用 MyBatis 的 XML 文件去写这些 SQL 会非常直接也好排查问题。对比来看JPA 在做简单的单表 CRUD 时效率很高但在权限系统里动态拼条件查用户列表、查角色菜单交集这种场景要么写原生 SQL 绕开它的对象导航要么写一堆 Specification 代码调试时还要打开 SQL 日志看它实际执行的语句中间隔了一层抽象总觉得心里没底。MyBatis 的另一个好处是 SQL 全在你手里生产环境慢查询定位时直接把日志里的 SQL 拿到数据库客户端执行加索引验证效果操作路径非常短。2.4 Spring Security 是这套组合的鉴权骨架Spring Boot 生态里做认证授权有两个选择Spring Security 和 Apache Shiro。标题既然用了 Spring Boot 全家桶那我建议直接用 Spring Security因为它和 Spring Boot 的自动配置结合最紧密社区资料也最多出问题基本都能搜到解决方案。Spring Security 的过滤器链机制是这个框架的核心。请求进到后端后会依次经过认证过滤器、授权过滤器链最后由ExceptionTranslationFilter统一处理认证异常和授权异常。在权限管理系统里我们要做的是配置哪些路径放行比如/auth/login、验证码接口、静态资源哪些路径需要认证然后通过PreAuthorize注解做方法级的细粒度控制。需要理解的另一个概念是UserDetailsService。这是 Spring Security 定义的一个接口作用是“根据用户名加载用户信息”。我们的做法是实现这个接口去sys_user表查用户然后查出这个用户的所有角色再根据角色查出所有菜单权限标识打包成一个LoginUser对象实现UserDetails接口放进 SecurityContext 里。Spring Security 的hasAuthority表达式实际上就是在这个上下文里判断当前用户持有的权限标识集合里有没有目标值。3. 用Spring Boot MyBatis落地后端权限建表、登录签发与注解鉴权3.1 数据库表设计五张核心表把RBAC模型落到MySQL先动手建表。这里给出的是经过多个项目验证的简化版结构字段尽量精简但覆盖了 RBAC 的核心链路。用户表、角色表、菜单表加上两张关联表这就是最小可用模型。CREATE TABLE sys_user ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password varchar(100) NOT NULL, nickname varchar(50) DEFAULT NULL, status tinyint(1) DEFAULT 1 COMMENT 1启用 0禁用, create_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4; CREATE TABLE sys_role ( id bigint(20) NOT NULL AUTO_INCREMENT, role_name varchar(50) NOT NULL, role_key varchar(50) NOT NULL COMMENT 权限标识如 admin, status tinyint(1) DEFAULT 1, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE sys_menu ( id bigint(20) NOT NULL AUTO_INCREMENT, parent_id bigint(20) DEFAULT 0 COMMENT 父菜单ID0表示根, menu_name varchar(50) NOT NULL, menu_type char(1) DEFAULT M COMMENT M目录 C菜单 F按钮, path varchar(100) DEFAULT NULL COMMENT 路由地址, component varchar(100) DEFAULT NULL COMMENT 前端组件路径, perms varchar(100) DEFAULT NULL COMMENT 权限标识如 system:user:add, order_num int(11) DEFAULT 0, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE sys_user_role ( user_id bigint(20) NOT NULL, role_id bigint(20) NOT NULL, PRIMARY KEY (user_id,role_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE sys_role_menu ( role_id bigint(20) NOT NULL, menu_id bigint(20) NOT NULL, PRIMARY KEY (role_id,menu_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段 DDL 里几个字段需要解释一下。menu_type用M/C/F区分目录、菜单、按钮目录没有component按钮没有path和component只有perms。按钮的perms字符串就是后端注解里hasAuthority要用的标识比如system:user:add这种“冒号分隔”的命名风格是通行约定前缀是模块名中间是实体名最后是操作。parent_id 0表示根节点是递归查询菜单树的起点。3.2 登录认证链路从接收用户名密码到签发JWT登录接口是整个权限系统的入口。用户提交用户名和密码后端校验通过后签发 JWT 返回给前端。下方代码是核心链路的一个简化版本省略了验证码和登录日志但保留了主流程。Service public class LoginServiceImpl implements LoginService { private final AuthenticationManager authenticationManager; private final TokenService tokenService; private final SysMenuMapper menuMapper; public LoginServiceImpl(AuthenticationManager authenticationManager, TokenService tokenService, SysMenuMapper menuMapper) { this.authenticationManager authenticationManager; this.tokenService tokenService; this.menuMapper menuMapper; } public LoginResult login(LoginBody loginBody) { // 1. 用AuthenticationManager执行认证失败会抛BadCredentialsException Authentication authentication authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( loginBody.getUsername(), loginBody.getPassword())); // 2. 认证通过后authentication里的principal是SecurityUser对象 SecurityUser securityUser (SecurityUser) authentication.getPrincipal(); // 3. 查询该用户拥有的全部菜单权限标识塞进Token里 ListString perms menuMapper.selectPermsByUserId(securityUser.getUserId()); securityUser.setPermissions(perms); // 4. 签发JWT String token tokenService.createToken(securityUser); return new LoginResult(token, securityUser.getUsername()); } }代码的逻辑分四步每步都有坑。第一步调用AuthenticationManager.authenticate它会自动触发UserDetailsService.loadUserByUsername去查库比对密码比对失败时抛出的异常会被全局异常处理器捕获统一返回“用户名或密码错误”。第二步强转Principal为我们自定义的SecurityUser这个对象在 3.3 节里会出现。第三步查菜单权限标识列表这里要注意不能只查该用户直接分配的权限要通过sys_user_role和sys_role_menu两张表关联查SQL 要用INNER JOIN或子查询不能漏掉角色这一层。第四步签发 TokenTokenService内部用JJWT库生成过期时间一般设 2 小时同时生成一个 refreshToken 或续期机制避免用户每两小时重新登录一次这个会在第 6 章展开讲。3.3 自定义UserDetailsService和JWT过滤器这是鉴权的两条腿Spring Security 要工作必须知道“如何从用户名查出用户信息”还要知道“如何从请求里解析出身份”。前者是UserDetailsService后者是我们的 JWT 认证过滤器。Service public class UserDetailsServiceImpl implements UserDetailsService { private final SysUserMapper userMapper; private final SysRoleMapper roleMapper; Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SysUser user userMapper.selectByUsername(username); if (user null) { throw new UsernameNotFoundException(用户不存在); } return new SecurityUser(user.getId(), user.getUsername(), user.getPassword(), roleMapper.selectRoleKeysByUserId(user.getId())); } } public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { private final TokenService tokenService; Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token request.getHeader(Authorization); if (StringUtils.hasText(token) token.startsWith(Bearer )) { SecurityUser user tokenService.parseToken(token.replace(Bearer , )); if (user ! null) { UsernamePasswordAuthenticationToken authentication new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); } } filterChain.doFilter(request, response); } }loadUserByUsername在登录认证时被AuthenticationManager调用角色用一个ListString存角色标识而不是权限标识。这里有个容易混淆的地方角色标识roleKey和权限标识perms是两个东西。hasRole(ADMIN)判断的是角色hasAuthority(system:user:add)判断的是权限。把角色标识转成ROLE_前缀的权限标识放在authorities里是为了让两种表达式都能用。JWT 过滤器里的OncePerRequestFilter保证了每次请求只执行一次。注意要先去掉 Authorization 头里的Bearer前缀再解析这是最常见的一个低级错误——忘了去掉前缀直接解析整个字符串导致签名验证失败请求被拦截。3.4 用PreAuthorize注解做方法级权限控制菜单和路由层面的控制只是第一层真正防住越权的是后端接口上的权限注解。业务接口在 Controller 方法上标注权限标识没有对应权限的用户直接收到 403。RestController RequestMapping(/system/user) public class SysUserController { GetMapping(/list) PreAuthorize(hasAuthority(system:user:list)) public TableDataInfo list(SysUserQuery query) { return userService.selectUserList(query); } PostMapping PreAuthorize(hasAuthority(system:user:add)) public AjaxResult add(RequestBody SysUser user) { return userService.insertUser(user); } PutMapping PreAuthorize(hasAuthority(system:user:edit)) public AjaxResult edit(RequestBody SysUser user) { return userService.updateUser(user); } DeleteMapping(/{ids}) PreAuthorize(hasAuthority(system:user:remove)) public AjaxResult remove(PathVariable Long[] ids) { return userService.deleteUserByIds(ids); } }PreAuthorize的生效前提是开启了方法级安全配置在启动类或配置类上加EnableGlobalMethodSecurity(prePostEnabled true)这是最容易被忽略的一步。权限表达式里除了hasAuthority还有hasRole(ADMIN)、hasAnyAuthority(...)、ss.hasPermi(system:user:add)等写法。我建议统一用hasAuthority因为权限标识本身就包含模块信息表达更精确不会出现两个模块都有admin角色时的语义冲突。4. 用Vue.js做动态路由与按钮级权限前端侧的权限收敛4.1 动态路由的实现思路菜单不是写死的是登录后算出来的前端权限的核心问题是不同用户登录后看到的菜单不一样输入一个无权限的 URL 要回到 404 页无权限的按钮要隐藏。这三个问题分别对应动态路由、路由守卫、自定义指令三类解法。常见做法是前端先写好一个路由常量表包含所有页面路由和组件映射关系但初始时不在路由器上注册。用户登录成功后请求后端接口拿到他自己的菜单树前端把菜单树处理成 Vue Router 需要的RouteRecordRaw数组然后通过router.addRoute()动态注册。为了防止刷新页面时路由丢失需要在全局守卫里判断如果 Vuex/状态管理里没有菜单信息就重新拉取菜单再放行。// 状态管理中的动态路由生成逻辑简化 function buildRoutes(menuTree) { const routes []; for (const menu of menuTree) { const route { path: menu.path, name: menu.menuName, component: resolveComponent(menu.component), meta: { title: menu.menuName, perms: menu.perms }, children: [] }; if (menu.children menu.children.length 0) { route.children menu.children.map(child buildRoutes([child])[0]); } routes.push(route); } return routes; } router.beforeEach((to, from, next) { const token getToken(); if (!token) { next(/login); return; } if (store.getters.menus.length 0) { store.dispatch(fetchMenus).then(() { const dynamicRoutes buildRoutes(store.getters.menus); dynamicRoutes.forEach(route router.addRoute(route)); next({ ...to, replace: true }); }).catch(() next(/login)); } else { next(); } });这段逻辑里值得注意的点有三个。一是resolveComponent里的组件路径来自后端component字段前端需要配置一个组件映射关系把字符串路径映射到实际的.vue文件Vite 的import.meta.glob或 Webpack 的require.context都能实现这一层映射。二是在动态路由注册完成后要next({ ...to, replace: true })重新导航一次否则当前地址被认为是未匹配路由会落到 404 页。三是每次刷新页面路由会恢复到初始状态所以必须在进入页面前的守卫里重新拉取并注册这套逻辑称为“刷新不掉线”的保障机制。4.2 按钮级控制自定义指令V-Permi的完整实现菜单控制了用户能看到哪些页面但页面里总有“新增”“删除”“导出”这类按钮需要按角色区分。这里既可以在组件里用v-if加权限判断也可以封装成一个自定义指令。指令的方式更优雅调用时只需要一行标记。// main.js 中注册自定义指令 import { usePermissionStore } from /store/permission; app.directive(permi, { mounted(el, binding) { const requiredPerms binding.value; if (!requiredPerms) return; const userPerms usePermissionStore().perms; const hasPermission userPerms.some(p requiredPerms.includes(p)); if (!hasPermission) { el.parentNode el.parentNode.removeChild(el); } } });template div el-button v-permi[system:user:add]新增用户/el-button el-button v-permi[system:user:export]导出/el-button /div /template这个指令的实现原理并不复杂。元素渲染时指令的mounted钩子拿到绑定的权限数组和当前用户的权限集合做一次交集判断没有权限就直接把 DOM 节点移除。注意这里用的是el.parentNode.removeChild(el)而不是el.style.display none身份验证不过的按钮不应该占位直接移除在视觉上更干净。依赖usePermissionStore时要注意权限集合必须在菜单拉取完成之前就已存入状态管理否则刷新页面首次渲染按钮时权限数据是空的按钮会被全部移除这是一个非常典型的时序问题。4.3 登录态维护与请求拦截Axios拦截器里的Token刷新逻辑前端所有请求都走统一的 Axios 实例在这个实例的请求拦截器里附加 Token在响应拦截器里统一处理 401 过期和 403 无权限。const service axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 10000 }); service.interceptors.request.use(config { const token getToken(); if (token) { config.headers.Authorization Bearer ${token}; } return config; }, error Promise.reject(error)); service.interceptors.response.use( response { const res response.data; if (res.code ! 200) { if (res.code 401) { // Token过期尝试用refreshToken续期后重放请求 return refreshTokenAndRetry(response.config); } if (res.code 403) { ElMessage.error(没有操作权限); return Promise.reject(new Error(Forbidden)); } ElMessage.error(res.msg || 请求失败); return Promise.reject(new Error(res.msg)); } return res; }, error { if (error.response error.response.status 401) { clearToken(); router.push(/login); } return Promise.reject(error); } );写请求拦截器时我踩过一个坑直接在响应拦截器方法名上做async/await导致返回的 Promise 结构不对调用方拿不到最终数据。正确写法是让拦截器返回一个Promise并且在续期成功后重放原始请求。续期逻辑的基本做法是后端提供一个POST /refreshToken接口校验旧 Token 是否过期但仍在有效期内比如 7 天免登录期内然后签发新 Token 返回。前端收到 401 后先调用这个接口拿新 Token再带着新 Token 重新发刚才失败的请求用户无感知地完成续期。5. 权限系统七个常见坑现象、原因与对策5.1 用户改了自己的权限立刻生效Redis缓存里却还是旧权限在权限改动后比如给某个用户增加了角色用户再去操作接口时仍然提示无权限但重新登录后权限又正常了。排查后发现在登录时将用户权限列表放入了 Redis后续每次请求直接认缓存没有做缓存失效处理。常见的解决思路是在权限变更的服务方法上调用缓存删除接口把该用户或该角色的缓存 key 删掉下次请求时再回源查库重建缓存。如果用的是 Spring Cache可以在角色菜单修改处加CacheEvict更精细的做法是维护一个权限版本号每次变更递增JWT 里带上版本号过滤器里比对不匹配就强制用户重新登录拉取新权限。5.2PreAuthorize注解不生效接口没有鉴权直接就返回了启动类上少了EnableGlobalMethodSecurity(prePostEnabled true)或者自定义 JWT 过滤器之后没有把它在SecurityFilterChain里正确加入到过滤器链的合适位置请求根本不会经过授权过滤器。检查方法看启动日志里是否出现 MethodSecurityInterceptor 相关的初始化信息再看过滤器链顺序JwtAuthenticationTokenFilter要在UsernamePasswordAuthenticationFilter之前否则 SecurityContext 里没有认证信息后面的PreAuthorize永远拿到的是一个匿名用户。常见的解决办法是在SecurityFilterChain配置中用addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)显式指定位置。5.3 JWT 里塞了太多数据导致请求头过大有些同学喜欢把用户完整信息、部门 id、所有角色对象都塞进 JWT Payload结果一个 Token 几 KB。虽然 JWT 通常放在 Authorization 头里日常请求没问题但当系统接入网关或某些云产品时对请求头大小有限的场景比如默认 8KB 或 16KB就会翻车。教训是 JWT 里只放能唯一定位用户的数据比如userId和uuid就够了其余信息一律通过 userId 反查数据库或缓存。这也顺带解决了一个问题用户改了自己的昵称或部门后Token 里的旧信息不会自动失效与其纠结“Token 里的数据过期”不如干脆别在 Token 里放大块业务数据。5.4 前端动态路由刷新后 404注册了但没生效用户停留在某个动态路由页面按 F5 刷新页面变成 404。这是因为动态路由是在登录后的路由守卫里addRoute的刷新时 Vue Router 会先解析当前 URL此时路由表还是初始状态只有静态路由匹配不到就落到/:pathMatch(.*)*兜底路由也就是 404 页。解决方式是在全局守卫里做“路由未注册就重新拉取”的判断拉取并注册完成后用next({ ...to })重导航一次。另外一个细节是动态路由注册后再次addRoute会警告重复命名建议注册前判断路由是否已存在或者用router.hasRoute(route.name)做一次校验。5.5 401 拦截器里出现无限重放请求响应拦截器里写了 401 处理执行了clearToken()同时业务代码里所有请求都自动带 Token于是一个界面打开时同时发了多路请求每个都返回 401每个都触发续期刷新 Token 接口被并发点爆。更隐蔽的是refreshTokenAndRetry里没有做去重同一个请求被重放多次。常见的规避手段是引入一个isRefreshing标志位续期请求进行中时后续到达的 401 请求先放进一个队列等新的 Token 回来后统一重放。这个方案在源码里通常要写上几十行但逻辑想一想就明白核心就是“并发等待同一个续期 Promise”。5.6 权限标识的命名混乱后端和前端各写一套数据库里perms存的是system:user:add前端组件里判断的是user:add后端注解写的是SYSTEM_USER_ADD三处对不上用户永远没有权限。这个问题说到底没有技术解法只能靠规范。第一个规则是权限标识的格式必须用小写加冒号前缀是模块名中缀是实体后缀是动作。第二个规则是前端、后端、数据库三处统一用一个常量文件维护推荐在前端维护一份 JSON后端用 Java 常量类两个文件的生成可以通过接口文档或代码生成工具解决但不要靠人来同步。第三个规则是审批制度权限标识的新增必须写进接口文档不能只在代码里加。6. 进阶给权限系统加上缓存、数据权限与操作日志从能用到扛得住前面的内容解决了权限系统从零到一的问题但真正投入到生产环境后还有三个问题绕不过去权限查询的性能、数据范围的控制、操作行为的审计。这三个能力直接影响系统能不能扛住多一些用户、能不能在出问题时快速回溯。下面说说我一般怎么处理。第一个问题是权限查询性能。用户的菜单树和按钮权限集合每次请求都查一遍数据库用户量上来之后是很明显的瓶颈。我一般会在用户登录时把他的菜单树和权限标识集合写入 Rediskey 用userId有效期跟着 Token 走Token 过期时就一起消失。后续请求在 JWT 过滤器里直接取 Redis 缓存不查库。角色权限变更时按roleId删缓存用户分配角色变更时按userId删缓存。如果系统使用 Spring Cache注意Cacheable注解默认生成的 key 可能包含参数列表配置时要指定 key 的生成规则避免缓存击穿和脏数据。第二个问题是数据权限。RBAC 解决了“能不能访问”的问题但没解决“能访问哪些数据”的问题。比如销售经理能看到自己部门的数据总经理能看到全公司数据这里需要在 SQL 层面加一层数据范围过滤。常见做法是在用户对象里存一个dataScope字段取值包括全部、本部门、本部门及以下、仅本人几种。查询时通过 MyBatis 的拦截器或手动拼接 SQL 条件自动附加WHERE dept_id IN (...)。这个方案在文档里写起来很简单落地时最大的坑是“表别名不一致导致过滤条件拼错”建议在DataScope注解里显式指定表别名。另外数据权限不要跟菜单权限混在一起它们是两个维度的东西菜单控制“能不能看见入口”数据权限控制“入口进去后能看到哪些行”。第三个问题是操作日志。权限系统的核心价值之一是可追溯重要操作新增用户、修改角色、删除菜单都应该留下记录。用 Spring AOP 定义一个Log注解标注在 Controller 方法上切面里记录操作用户、操作时间、请求 IP、方法描述、请求参数和返回结果。异步写入日志表时要注意日志表不要和业务表放到同一个事务里避免业务失败时日志也回滚另一个细节是请求参数里如果包含密码字段要在切面里做脱敏。更稳的做法是同时让前端把当前用户的 UA 和页面路径一并传给后端方便排查时还原操作路径。最后说一个我自己的经验。写完这套权限系统后发现真正的维护成本不在代码里而在权限标识的命名规范和角色分配审批流程上。代码里留再多扩展点如果权限标识混乱、角色文档缺失三个月后自己都分不清更别提新同事接手。所以从第一天开始就要把权限标识清单当作文档维护角色对应的权限矩阵按期导出核对。这套 Spring Boot MyBatis Vue.js 的组合只要模型设计清晰、边界划清楚把权限标识当工程规范而不是代码细节来对待就能一直稳下去。希望这些内容对你有帮助。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑