ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Spring Boot + Vue.js 权限管理系统实战:RBAC 模型与动态路由落地

Spring Boot + Vue.js 权限管理系统实战:RBAC 模型与动态路由落地

简介:这份资源是一套基于 SpringBoot、MyBatis 与 Vue.js 构建的权限管理系统完整工程,面向正在学习 Java 全栈开发、需要实战权限控制模块的开发者与课程设计者,可用于理解前后端分离架构下用户、角色与资源之间的权限映射关系。压缩包共 312 个文件,约 292.22MB,以 187 个 XML 映射与配置文件、38 个 Java 源码、34 个编译类文件为主,辅以 jar 依赖、yml 与 properties 配置、SQL 脚本及数据库模型文件,覆盖后端服务、持久层映射与前端工程等层次。系统围绕用户管理、角色管理、角色资源与权限分配、日志记录展开,并设有回收站机制,可恢复误删数据,便于读者对照源码梳理权限校验流程与数据操作逻辑。目前已有 1463 人学习下载,适合作为课程设计、毕业设计或权限模块二次开发的参考工程。

1. 权限管理系统:一个 Spring Boot + MyBatis + Vue.js 的落地样本

后台管理系统几乎是每个开发者绕不开的一类项目,而权限管理又是其中最容易做歪的部分。我见过太多项目把角色判断散落在各个 Controller 里,改一个菜单权限要翻十几个文件。这套基于 Spring Boot + MyBatis + Vue.js 的权限管理系统,把用户、角色、菜单、按钮权限拆成清晰的几层,前端用 Vue 做动态路由和按钮级控制,后端用 Spring Boot 提供 REST 接口,MyBatis 负责数据持久化。它适合两类人:一是想快速搭一个后台骨架的开发者,二是想搞清楚 RBAC 模型怎么在前后端同时落地的人。下面我按「能干什么、怎么跑、怎么改」的顺序,把这份源码拆开讲。

2. 技术栈与目录结构:先看清骨架再动手

2.1 后端技术选型与依赖关系

后端是标准的 Spring Boot 分层结构,核心依赖包括 Spring Boot Web、MyBatis、MySQL 驱动、Spring Security(或自研拦截器做鉴权)、JWT 做无状态登录。我一般会把权限校验放在过滤器或拦截器层,而不是每个接口里手写判断。项目里常见的做法是:登录成功后签发 token,前端每次请求带上 token,后端解析出用户 ID 和角色,再查该角色对应的菜单和按钮权限。

<!-- pom.xml 关键依赖片段 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>

这段依赖里,mybatis-spring-boot-starter负责把 Mapper 接口自动注册成 Bean,jjwt用来生成和解析 token。版本号不用死记,关键是保证 Spring Boot 版本和 MyBatis starter 版本兼容,否则启动时会报NoSuchMethodError。数据库连接配置放在application.yml,常见写法是配url、username、password和mapper-locations,其中mapper-locations指向classpath:mapper/*.xml,漏了这行 XML 映射文件就加载不到。

2.2 前端目录与动态路由机制

前端是 Vue.js 单页应用,目录大致分为api(接口封装)、router(路由)、store(状态管理)、views(页面)、components(公共组件)、utils(工具函数)。权限相关的核心在router和store两个目录:登录后从后端拉取当前用户的菜单树,前端把菜单树转换成路由配置,用router.addRoute动态挂载。

// router/permission.js 动态路由核心逻辑 const menuList = await getUserMenus() // 后端返回的菜单树 const routes = filterAsyncRoutes(menuList) // 转成路由对象 routes.forEach(route => { router.addRoute(route) // 动态挂载 })

filterAsyncRoutes的作用是把后端返回的菜单数据(含path、component、meta)映射成 Vue Router 能识别的路由对象,component字段通常用() => import('@/views/' + component)做懒加载。这里有个细节:后端返回的component是字符串路径,前端需要拼上@/views/前缀,拼错了页面就是白屏,控制台会报Cannot find module。

2.3 数据库表结构与 RBAC 模型

权限系统的数据模型是整套逻辑的地基。典型五张表:sys_user(用户)、sys_role(角色)、sys_menu(菜单/权限)、sys_user_role(用户角色关联)、sys_role_menu(角色菜单关联)。用户和角色是多对多,角色和菜单也是多对多,菜单表里用type字段区分目录、菜单、按钮三种类型。

CREATE TABLE sys_menu ( id BIGINT PRIMARY KEY AUTO_INCREMENT, parent_id BIGINT DEFAULT 0, name VARCHAR(50) NOT NULL, path VARCHAR(100), component VARCHAR(100), perms VARCHAR(100), -- 按钮权限标识,如 system:user:add type TINYINT NOT NULL, -- 0目录 1菜单 2按钮 icon VARCHAR(50), sort INT DEFAULT 0 );

perms字段是按钮级权限的关键,前端用v-permission指令判断当前用户是否拥有该标识,后端在接口上加注解校验。type字段决定这行数据是渲染成侧边栏目录、可点击菜单,还是隐藏的按钮权限点。parent_id为 0 表示顶级,递归查询就能拼出整棵树。

3. 核心模块拆解:登录、鉴权与菜单渲染的完整链路

3.1 登录认证:从密码校验到 token 签发

登录接口接收用户名和密码,先查用户,再比对密码(常见做法是 BCrypt 加密存储,比对时用matches方法),校验通过后查该用户的角色和权限标识,塞进 token 的 payload。

@PostMapping("/login") public Result login(@RequestBody LoginDTO dto) { User user = userMapper.selectByUsername(dto.getUsername()); if (user == null || !passwordEncoder.matches(dto.getPassword(), user.getPassword())) { return Result.fail("用户名或密码错误"); } List<String> perms = menuMapper.selectPermsByUserId(user.getId()); String token = JwtUtil.createToken(user.getId(), perms); return Result.ok(token); }

passwordEncoder.matches是 Spring Security 提供的明文与密文比对方法,不要自己写字符串比较。selectPermsByUserId是一条三表关联查询,从用户角色关联表一路 join 到菜单表,取出所有perms非空的记录。token 里放perms是为了后续鉴权时不用每次查库,但要注意 token 体积,权限多的时候 payload 会变大,常见做法是只放用户 ID,权限走缓存。

3.2 接口鉴权:拦截器与注解配合

后端鉴权一般用拦截器统一处理,放行登录接口和静态资源,其余请求解析 token,把用户信息放进ThreadLocal,再校验接口所需权限。

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token = request.getHeader("Authorization"); if (StringUtils.isEmpty(token)) { throw new AuthException("未登录"); } Claims claims = JwtUtil.parseToken(token); UserContext.set(claims.getSubject(), claims.get("perms", List.class)); return true; }

preHandle里解析失败会抛异常,交给全局异常处理器统一返回 401。UserContext是基于ThreadLocal的上下文工具,请求结束时记得在afterCompletion里remove(),否则线程池复用会导致用户信息串号,这是血泪经验。按钮级权限则在 Controller 方法上加自定义注解,配合 AOP 或拦截器读取注解值,和当前用户权限列表比对。

3.3 菜单树构建与前端渲染

后端查菜单时按parent_id递归组装成树,前端拿到树后一部分转路由,一部分渲染侧边栏。

public List<MenuVO> buildTree(List<Menu> menus, Long parentId) { return menus.stream() .filter(m -> parentId.equals(m.getParentId())) .map(m -> { MenuVO vo = new MenuVO(); BeanUtils.copyProperties(m, vo); vo.setChildren(buildTree(menus, m.getId())); return vo; }) .collect(Collectors.toList()); }

这段递归的关键是先一次性查出该用户所有菜单,再在内存里按parentId分组,避免每层都查一次库。BeanUtils.copyProperties做实体到 VO 的拷贝,字段名要一致。前端侧边栏组件递归渲染children,路由则只取type为菜单的节点,按钮节点不生成路由。

3.4 按钮级权限指令

前端用自定义指令控制按钮显隐,避免在每个页面写一堆v-if。

// directives/permission.js export default { mounted(el, binding) { const perms = store.getters.perms // 当前用户权限列表 if (!perms.includes(binding.value)) { el.parentNode && el.parentNode.removeChild(el) } } }

binding.value就是按钮上写的权限标识,比如v-permission="'system:user:add'"。指令在mounted阶段判断,没有权限直接移除 DOM。注意这只是视觉隐藏,真正的安全边界在后端接口鉴权,前端指令防的是误操作,不是恶意请求。

4. 本地运行与联调:从零把项目跑起来

4.1 环境准备与数据库初始化

先装 JDK 8 或 11、Maven、Node.js(建议 14 以上)、MySQL 5.7 或 8.0。建库后导入项目里的sql脚本,脚本一般包含建表语句和初始管理员数据。改application.yml里的数据库连接,用户名密码换成自己的。

mysql -u root -p CREATE DATABASE permission_system DEFAULT CHARSET utf8mb4; use permission_system; source /path/to/init.sql;

utf8mb4是为了支持完整字符集,别用utf8,否则某些特殊字符会报错。导入后确认sys_user表里有初始账号,密码是加密后的密文,登录时用脚本注释里写的明文。

4.2 后端启动与接口验证

在项目根目录执行mvn spring-boot:run,或者打包后java -jar。启动日志里看到 Tomcat 端口和 MyBatis 加载的 Mapper 数量就算成功。用 Postman 调登录接口验证。

curl -X POST http://localhost:8080/login \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"123456"}'

返回体里应该有 token。拿 token 调一个需要鉴权的接口,请求头加Authorization: token值,能返回数据说明鉴权链路通了。如果返回 401,先看拦截器放行规则有没有把该路径漏掉。

4.3 前端启动与跨域处理

前端目录执行npm install再npm run serve。开发环境跨域常见做法是在vue.config.js里配devServer.proxy,把/api代理到后端端口。

// vue.config.js module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, pathRewrite: { '^/api': '' } } } } }

changeOrigin设为 true 是为了修改请求头里的 host,pathRewrite把/api前缀去掉再转发。前端 axios 的baseURL配成/api,这样开发和生产切换只改代理配置。登录成功后 token 存 localStorage,axios 请求拦截器统一带上。

5. 避坑与排查:几个容易翻车的地方

5.1 动态路由刷新后 404

现象是登录后能进首页,刷新页面就白屏或 404。原因是动态路由是运行时addRoute加的,刷新后路由表重建,还没加就匹配了。解决是在路由守卫里判断,如果store里没有菜单数据,先拉菜单再next({ ...to, replace: true })重新进入。

5.2 ThreadLocal 用户信息串号

现象是偶发接口返回别人的数据。原因是拦截器里set了但没在afterCompletion里remove,线程池复用线程时残留了上一个请求的用户。解决是务必在afterCompletion里调UserContext.clear(),这是必须养成的习惯。

5.3 MyBatis 下划线转驼峰失效

现象是查出来的对象字段全是 null。原因是数据库字段create_time和实体createTime没映射上。解决是在application.yml里配mybatis.configuration.map-underscore-to-camel-case: true,或者在 XML 里写resultMap显式映射。

5.4 按钮权限指令不生效

现象是加了v-permission按钮还是显示。原因是权限列表在指令mounted时还没加载完,或者指令没全局注册。解决是确保菜单数据在路由守卫里先加载,指令在main.js里Vue.directive('permission', permission)注册。

5.5 菜单树递归死循环

现象是接口一直不返回或栈溢出。原因是数据里存在parent_id指向自己或形成环。解决是建表时加约束,查询时加层级上限,或者在递归里记录已访问 ID 做去重。

6. 二次开发:把权限系统改成你自己的业务骨架

拿到这套源码后,最值钱的改造点是把「通用权限」和「业务模块」解耦。我的习惯是新建一个biz包,业务 Controller 只依赖权限注解,不碰用户角色逻辑。新增一个业务模块时,先在sys_menu里插一条菜单记录和若干按钮权限,再写对应的 Vue 页面,路由会自动挂上,不用改路由文件。

具体做法分三步。第一步,在菜单表插入数据,type为 1 的是页面菜单,type为 2 的是按钮,perms按模块:实体:动作命名,比如order:list:export。第二步,后端写 Controller,方法上加@RequiresPerm("order:list:export")这类自定义注解,拦截器统一校验。第三步,前端页面里按钮加v-permission="'order:list:export'",页面组件路径和菜单表component字段对应上。

验证改造是否成功,我一般走一遍「新角色只给部分权限」的流程:建一个测试角色,只勾选查询权限,登录后确认新增和删除按钮不显示,直接调删除接口返回 403。这一步能同时验证前端指令和后端拦截器,缺一个都会露馅。

改造点改哪里验证方式
新增业务菜单sys_menu插数据登录后侧边栏出现
接口权限Controller 加注解无权限调用返回 403
按钮权限页面加指令无权限按钮不渲染
数据权限Mapper 加条件只能看到自己数据

数据权限是这套骨架没覆盖的部分,二次开发时经常要补。常见做法是在 Mapper 查询里拼create_by = 当前用户或按部门过滤,别在 Service 里用if判断,容易漏。我踩过的坑是数据权限和按钮权限混在一起写,后来发现两者维度不同,按钮管「能不能点」,数据管「能看到哪些行」,分开设计才不容易乱。

从那以后我每次接后台项目,都先把菜单表和权限标识定死,再写业务代码,返工率明显下降。希望这套拆解能帮到你,少走几个我走过的弯路。

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

返回列表