
简介这是一套基于JavaMySQLSSM框架开发的权限管理系统完整源码面向计算机专业学生、Java初学者及需要完成课程设计或毕业设计的人群帮助解决权限分配、角色管理与菜单维护等典型业务场景的开发难题。压缩包共602个文件约31.44MB涵盖89个java源文件、24个jsp页面、29个xml配置、65个jar依赖包以及js、css、gif等前端与静态资源并附带sql建表脚本结构完整可直接导入运行。系统采用B/S体系与标准MVC模式划分为表现层、controller、service、dao四层核心亮点在于以角色为表头、菜单为首列动态加载权限通过表格内开关实现在线分配免去页面跳转同时以树形结构呈现角色与菜单整合增删改操作并支持菜单图标与按钮级权限管理。目前已有117人学习下载适合作为SSM框架入门实战与毕设参考的完整案例。1. 从一份 SSM 权限系统源码说起它到底解决了什么问题很多 Java 开发工程师在面试或接私活时都会遇到同一个需求给一套后台系统加上「谁能看哪个菜单、谁能点哪个按钮、谁能改哪条数据」的控制。这就是权限管理系统要干的事。而 SSMSpring SpringMVC MyBatis作为国内存量最大的 Java Web 技术栈之一配合 MySQL 做一套完整的权限管理源码几乎是每个 Java 开发工程师绕不开的练手项目也是面试题里高频出现的场景题。这套源码通常要解决三层问题认证你是谁、授权你能干什么、数据范围你能看哪些数据。落到代码上就是用户表、角色表、权限表、菜单表、用户角色关联表、角色权限关联表这几张核心表加上登录拦截、注解鉴权、菜单动态渲染这几段逻辑。它适合两类人一是刚学完 SSM 想找个完整项目练手的 Java 基础学习者二是需要在现有系统里快速落地一套 RBAC 权限模块的开发者。下面我按「表怎么设计 → 代码怎么写 → 坑在哪 → 怎么验证」的顺序把一套能跑起来的方案讲清楚。2. 权限模型与数据库设计RBAC 表结构怎么落地到 MySQL2.1 为什么选 RBAC 而不是直接给用户配权限最常见的做法是 RBAC基于角色的访问控制。核心思路是不直接把权限给用户而是用户关联角色角色关联权限。这样做的好处是当公司有 200 个运营人员、权限完全一样时你只需要维护一个「运营」角色而不是给 200 个人逐个配权限。用户离职或转岗改一下角色关联就行不用动权限表。RBAC 还分 RBAC0 到 RBAC3。RBAC0 是最基础的「用户-角色-权限」三层RBAC1 加了角色继承比如「高级运营」继承「运营」的所有权限RBAC2 加了职责分离约束比如「制单」和「审核」不能给同一个人RBAC3 是 12 的合集。对于绝大多数后台系统RBAC0 加一点数据范围控制就够了别一上来就上 RBAC3维护成本会翻倍。2.2 六张核心表的字段与建表语句下面这套表结构是我在多个项目里反复用过的版本字段不多但够用。注意 MySQL 5.7 和 8.0 在字符集和默认值上有些差异建表时统一用 utf8mb4避免中文和特殊符号乱码。-- 用户表 CREATE TABLE sys_user ( user_id BIGINT NOT NULL AUTO_INCREMENT COMMENT 用户ID, username VARCHAR(50) NOT NULL COMMENT 登录账号, password VARCHAR(100) NOT NULL COMMENT BCrypt加密后的密码, nickname VARCHAR(50) DEFAULT NULL COMMENT 昵称, status TINYINT NOT NULL DEFAULT 1 COMMENT 1正常 0停用, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (user_id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 角色表 CREATE TABLE sys_role ( role_id BIGINT NOT NULL AUTO_INCREMENT, role_name VARCHAR(50) NOT NULL COMMENT 角色名如 运营, role_key VARCHAR(50) NOT NULL COMMENT 角色标识如 operator, status TINYINT NOT NULL DEFAULT 1, PRIMARY KEY (role_id), UNIQUE KEY uk_role_key (role_key) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT角色表; -- 菜单/权限表菜单和按钮统一用这张表用 menu_type 区分 CREATE TABLE sys_menu ( menu_id BIGINT NOT NULL AUTO_INCREMENT, parent_id BIGINT NOT NULL DEFAULT 0 COMMENT 父菜单ID0为顶级, menu_name VARCHAR(50) NOT NULL COMMENT 菜单名, menu_type CHAR(1) NOT NULL COMMENT M目录 C菜单 F按钮, perms VARCHAR(100) DEFAULT NULL COMMENT 权限标识如 system:user:add, path VARCHAR(200) DEFAULT NULL COMMENT 路由地址, order_num INT NOT NULL DEFAULT 0, PRIMARY KEY (menu_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT菜单权限表; -- 用户角色关联表 CREATE TABLE sys_user_role ( user_id BIGINT NOT NULL, role_id BIGINT NOT NULL, PRIMARY KEY (user_id, role_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户角色关联; -- 角色菜单关联表 CREATE TABLE sys_role_menu ( role_id BIGINT NOT NULL, menu_id BIGINT NOT NULL, PRIMARY KEY (role_id, menu_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT角色菜单关联;字段说明几个关键点perms是权限标识格式建议用「模块:功能:操作」比如system:user:add后端鉴权时直接比对字符串menu_type用 M/C/F 区分目录、菜单、按钮前端渲染菜单时只取 M 和 C按钮权限单独用perms判断关联表用联合主键天然防重复。密码字段长度给 100因为 BCrypt 加密后的串是 60 位留点余量。2.3 数据范围控制的表怎么加如果需求里还有「部门经理只能看本部门数据」那就再加两张表部门表sys_dept和角色部门关联表sys_role_dept。查询时在 SQL 里拼一个dept_id IN (...)的条件。这块别做太复杂常见做法是给角色加一个data_scope字段1 全部、2 本部门、3 本部门及以下、4 仅本人然后在 MyBatis 的拦截器或 XML 里动态拼条件。行级权限 Java 实现的核心就在这一步后面第 5 章会展开。3. SSM 整合与登录鉴权从 Spring 配置到拦截器落地3.1 SSM 三件套的依赖与配置要点SSM 整合的坑大多出在版本冲突和配置遗漏上。我一般用 Maven 管理依赖Spring 5.x 配 MyBatis 3.5.xMySQL 驱动用 8.0.x对应com.mysql.cj.jdbc.Driver。下面是最小可用的 pom 片段properties spring.version5.3.30/spring.version mybatis.version3.5.13/mybatis.version /properties dependencies dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version${spring.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.1.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency /dependencies参数说明mybatis-spring的版本要和 MyBatis 主版本匹配2.x 对应 MyBatis 3.5Druid 连接池的validationQuery在 MySQL 8 下写SELECT 1即可。Spring 配置里最容易漏的是context:component-scan的包路径和MapperScannerConfigurer的basePackage漏了就会报「No qualifying bean」。3.2 登录接口与密码加密登录逻辑本身不复杂查用户、比对密码、生成 token、返回。关键是密码不能明文存。常见做法是用 BCrypt每次加密结果不同但校验能通过。Service public class LoginService { Autowired private SysUserMapper userMapper; public String login(String username, String rawPassword) { // 1. 按账号查用户 SysUser user userMapper.selectByUsername(username); if (user null) { throw new RuntimeException(账号或密码错误); } // 2. 校验状态 if (user.getStatus() 0) { throw new RuntimeException(账号已停用); } // 3. BCrypt 比对密码 if (!BCrypt.checkpw(rawPassword, user.getPassword())) { throw new RuntimeException(账号或密码错误); } // 4. 生成 token这里用 UUID 简化生产建议 JWT String token UUID.randomUUID().toString().replace(-, ); // 5. 存入 Redis 或本地缓存设置过期时间 TokenCache.put(token, user.getUserId()); return token; } }逻辑说明第 1 步查不到用户和第 3 步密码错误返回同样的提示避免暴露账号是否存在第 4 步用 UUID 只是演示生产环境建议用 JWT 并把用户 ID、过期时间签进去第 5 步的缓存过期时间一般设 30 分钟到 2 小时配合前端续期。参数上BCrypt 的checkpw第一个参数是明文第二个是数据库里的密文顺序别写反。3.3 拦截器实现接口级鉴权登录之后每个请求都要校验 token 和权限。用 SpringMVC 的HandlerInterceptor做统一拦截public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口和静态资源 String uri request.getRequestURI(); if (uri.contains(/login) || uri.startsWith(/static)) { return true; } // 取 token String token request.getHeader(Authorization); Long userId TokenCache.get(token); if (userId null) { response.setStatus(401); return false; } // 注解鉴权看方法上有没有 RequiresPerm if (handler instanceof HandlerMethod) { HandlerMethod hm (HandlerMethod) handler; RequiresPerm perm hm.getMethodAnnotation(RequiresPerm.class); if (perm ! null) { SetString userPerms PermCache.get(userId); if (!userPerms.contains(perm.value())) { response.setStatus(403); return false; } } } return true; } }逻辑说明先放行登录和静态资源避免死循环token 无效返回 401权限不足返回 403前端据此跳转不同页面RequiresPerm是自定义注解值就是数据库里的perms字段。参数上PermCache建议在用户登录时一次性把该用户所有权限查出来缓存别每次请求都查库。注册拦截器时记得在WebMvcConfigurer里addInterceptor并配addPathPatterns(/**)。4. 避坑与排查SSM 权限系统上线前必须过的 5 道坎4.1 菜单树递归查库导致 N1 查询现象登录后加载菜单特别慢日志里看到几十条select * from sys_menu where parent_id ?。 原因递归查子菜单时每层都查一次库菜单一多就是 N1。 解决一次性查出所有菜单在 Java 内存里用 Map 组装树。代码大致是先把所有菜单按parent_id分组再从根节点递归拼装。数据量不大时这是最省事的做法。4.2 权限标识大小写不一致导致鉴权失败现象数据库里配的是system:user:add注解里写的是system:User:add结果 403。 原因字符串比对区分大小写手写容易出错。 解决统一用小写加冒号分隔并在团队里约定死。更稳的做法是定义常量类注解里引用常量而不是硬编码字符串。4.3 MySQL 8 连接报 SSL 警告或时区错误现象启动时报Establishing SSL connection without servers identity verification is not recommended或者时间差 8 小时。 原因MySQL 8 默认要求 SSL且时区默认 UTC。 解决JDBC URL 加上useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue。生产环境该开 SSL 还是要开本地开发图省事关掉即可。4.4 拦截器把 OPTIONS 预检请求拦掉了现象前端跨域请求全部失败浏览器控制台报 CORS 错误。 原因跨域时浏览器先发 OPTIONS 预检拦截器没放行直接返回 401。 解决在preHandle里判断HttpMethod.OPTIONS直接返回 true或者用 Spring 的CorsFilter统一处理别在拦截器里跟预检请求较劲。4.5 角色删除后关联表残留脏数据现象删了角色用户登录后还能看到该角色对应的菜单。 原因只删了sys_role没删sys_user_role和sys_role_menu。 解决删除角色时用事务包住三条 delete或者建表时加外键级联删除。我一般倾向代码里显式删外键在分库分表场景下反而是累赘。5. 进阶技巧用 MyBatis 拦截器实现行级数据权限行级权限是权限系统里最容易做糙的部分。常见做法是在每个查询方法里手动拼dept_id条件但这样侵入性太强改一处漏一处。更优雅的方案是用 MyBatis 的Interceptor拦截Executor.query在 SQL 执行前动态改写。Intercepts({ Signature(type Executor.class, method query, args {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}) }) public class DataScopeInterceptor implements Interceptor { Override public Object intercept(Invocation invocation) throws Throwable { MappedStatement ms (MappedStatement) invocation.getArgs()[0]; Object param invocation.getArgs()[1]; // 只处理带 DataScope 注解的查询 DataScope ds getDataScopeAnnotation(ms.getId()); if (ds null) { return invocation.proceed(); } // 取当前登录用户的数据范围 Long userId UserContext.getUserId(); String scopeSql buildScopeSql(userId, ds.deptAlias()); // 用反射改写 SQL拼上 AND (dept_id IN (...)) BoundSql boundSql ms.getBoundSql(param); String newSql boundSql.getSql() AND scopeSql; // 反射替换 BoundSql 中的 sql 字段 ReflectUtil.setFieldValue(boundSql, sql, newSql); return invocation.proceed(); } }逻辑说明拦截器只对标注了DataScope的方法生效避免误伤buildScopeSql根据当前用户的角色查出data_scope生成对应的dept_id IN (...)或user_id ?片段用反射改写BoundSql的sql字段是 MyBatis 插件开发的常规操作注意 JDK 版本对反射的限制JDK 17 以上需要--add-opens。参数上deptAlias用来指定部门字段在 SQL 里的表别名多表联查时必填否则会报字段歧义。这个方案的好处是业务代码零侵入坏处是调试时 SQL 不直观建议在拦截器里把改写后的 SQL 打到 debug 日志方便排查。验证方法很简单建两个部门各一个用户给角色配不同的data_scope分别登录查列表看返回条数是否符合预期。我踩过的坑是忘了给管理员角色配「全部数据」范围结果管理员登录后只能看到自己部门的数据排查了半天才发现是data_scope默认值设成了 4。最后说个习惯每次改完权限相关代码我都会用不同角色的账号各跑一遍核心接口而不是只测管理员。管理员权限太大很多越权问题在管理员视角下根本暴露不出来。希望帮到你。本文还有配套的精品资源点击获取