手写SSM权限管理系统:从数据库表设计到拦截器实现完整解析

发布时间:2026/9/16 5:27:39
手写SSM权限管理系统:从数据库表设计到拦截器实现完整解析 简介一套基于JavaMySQLSSMSpringSpringMVCMyBatis的权限管理系统完整源码采用B/S架构与标准MVC分层适用于毕业设计、SSM框架学习及后台权限模块二次开发。系统以角色为表头、菜单为首列支持在线分配权限与动态加载角色/菜单/权限表中直接设置权限开关同时以树形结构管理角色与菜单整合增删改、菜单图标及按钮权限配置交互简洁高效。资源共602个文件涵盖89个Java源码、24个JSP页面、115个JS脚本、29个XML配置、65个Jar依赖及SQL脚本等压缩包31.44MB代码结构清晰便于导入IDE直接部署调试。核心控制层覆盖用户、角色、菜单、权限管理并附带验证码工具等通用组件可直接运行或作为毕设项目参考。已有115人学习下载适合需要快速搭建权限管理功能或借鉴SSM整合细节的开发者。1. 权限管理系统为什么JavaMySQLSSM这套老组合仍然值得手写一份完整源码“权限管理系统”在公司后台和毕设项目里出现频率最高但也是最容易被低估的需求。很多人以为它就是一个登录页加几个if判断真正拆开看才发现一套能交付的权限管理系统至少包含用户管理、角色管理、菜单管理、按钮权限、操作日志和动态路由代码量堪比一个小型电商后台。既然Spring Security已经封装好了为什么还要自己搭一套JavaMySQLSSM的权限管理系统原因有三个老项目技术栈固定SSM的拦截器机制能让你看清权限校验每一步发生在哪个环节面试官问到“RBAC怎么落库”“URL越权怎么防”时只有亲手写过才能答得出细节这类源码扩展起来特别顺手数据源换成Druid、缓存换成Redis都不需要推翻重来。本文就按实际项目的组织方式从数据库、框架配置、登录认证到接口拦截逐一走通。2. 权限管理系统的MySQL表结构五张核心表与字段参数2.1 先定RBAC模型再写代码权限管理系统的地基在数据库数据模型定不下来后面所有拦截判断都是空中楼阁。业界最通用的是RBAC模型用户关联角色角色关联权限用户通过角色间接获得权限。Spring Security用这套模型Shiro也用这套模型自己用SSM实现时同样跑不脱这套设计。RBAC的好处是改权限不用动用户表新入职一个运营就分配运营角色离职了把关联关系一删即可权限变更成本全在角色这一层。需要注意RBAC的边界它管不了“某个用户对某个具体订单有单独查看权”这类行级数据权限。做权限管理系统之前要把边界划清楚——菜单权限、按钮权限、接口权限走RBAC行级数据权限需要另做一套基于规则或基于所有者的过滤机制不要混在一张表里硬设计。2.2 建表SQL用户、角色、权限、关联表四层结构这五张表是权限管理系统的骨架建表脚本可以直接拿去用。MySQL 5.7和8.0通用存储引擎统一InnoDB字符集统一utf8mb4。CREATE TABLE t_user ( user_id int(11) NOT NULL AUTO_INCREMENT COMMENT 用户ID, username varchar(50) NOT NULL COMMENT 登录账号, password varchar(128) NOT NULL COMMENT 密码(MD5加盐哈希), real_name varchar(20) DEFAULT NULL COMMENT 真实姓名, status tinyint(1) DEFAULT 1 COMMENT 状态:1启用 0禁用, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, deleted tinyint(1) DEFAULT 0 COMMENT 逻辑删除:0未删 1已删, PRIMARY KEY (user_id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE t_role ( role_id int(11) NOT NULL AUTO_INCREMENT COMMENT 角色ID, role_name varchar(50) NOT NULL COMMENT 角色名称, role_code varchar(50) NOT NULL COMMENT 角色编码, status tinyint(1) DEFAULT 1 COMMENT 状态, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, deleted tinyint(1) DEFAULT 0, PRIMARY KEY (role_id), UNIQUE KEY uk_role_code (role_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT角色表; CREATE TABLE t_permission ( perm_id int(11) NOT NULL AUTO_INCREMENT COMMENT 权限ID, perm_name varchar(50) NOT NULL COMMENT 权限名称, perm_code varchar(100) NOT NULL COMMENT 权限编码, perm_type tinyint(1) NOT NULL COMMENT 类型:1目录 2菜单 3按钮 4接口, parent_id int(11) DEFAULT 0 COMMENT 父节点ID, url varchar(200) DEFAULT NULL COMMENT 菜单或接口路径, icon varchar(50) DEFAULT NULL COMMENT 图标, sort_order int(4) DEFAULT 0 COMMENT 排序, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (perm_id), KEY idx_parent_id (parent_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT权限表; CREATE TABLE t_user_role ( id int(11) NOT NULL AUTO_INCREMENT, user_id int(11) NOT NULL COMMENT 用户ID, role_id int(11) NOT NULL COMMENT 角色ID, PRIMARY KEY (id), UNIQUE KEY uk_user_role (user_id,role_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户-角色关联表; CREATE TABLE t_role_permission ( id int(11) NOT NULL AUTO_INCREMENT, role_id int(11) NOT NULL COMMENT 角色ID, perm_id int(11) NOT NULL COMMENT 权限ID, PRIMARY KEY (id), UNIQUE KEY uk_role_perm (role_id,perm_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT角色-权限关联表;刚才建表SQL里有几个参数值得单独说明。password字段长度设为128而不是32是因为MD5加盐后通常是32位十六进制但后续如果要升级为多重哈希算法或兼容其他加密方式32位长度会卡死扩展空间。deleted逻辑删除字段是必须的权限表被角色关联引用物理删行会导致历史关联数据悬空账都没法对。两张关联表都建了联合唯一索引比如uk_user_role防止重复插入同一对用户和角色这个索引在批量分配角色时就是唯一约束的兜底。这里没有写任何物理外键只用索引和业务代码保证关联完整性。外键在权限管理系统里弊大于利批量删除、数据迁移、分库分表时全是绊脚石而且大部分团队规范就是禁用物理外键。日常用Navicat for MySQL打开这五张表时重点看两张关联表的联合主键是否生效很多权限错乱问题的根因就是关联表里出现了重复数据。2.3 权限字典与初始化数据菜单、按钮、接口都要入库t_permission表里的perm_type字段是这套系统的核心设计它把权限粒度分成了四个层级1目录、2菜单、3按钮、4接口。目录和菜单用于前端动态生成左侧导航按钮用于页面里的操作按钮显隐接口用于后端URL级权限匹配。四个类型放在同一张表而不是拆成多张表是为了让树形结构的查询和排序保持一致用一个parent_id就能拼出完整权限树。初始化数据必须植入一条超级管理员角色常见做法是把角色编码固定为admin绑定全部权限。用一条关联插入就可以完成初始化不需要写存储过程INSERT INTO t_role (role_name, role_code, status) VALUES (系统管理员, admin, 1); INSERT INTO t_permission (perm_name, perm_code, perm_type, url) VALUES (用户管理, user:manage, 2, /user/list), (新增用户, user:add, 3, NULL), (编辑用户, user:edit, 3, NULL), (删除用户, user:delete, 3, NULL), (查询用户接口, user:list:api, 4, /api/user/list); INSERT INTO t_role_permission (role_id, perm_id) SELECT 1, perm_id FROM t_permission;最后一条INSERT ... SELECT是权限管理系统初始化时很常用的小技巧直接把当前全部权限挂到超级管理员角色上后面新增权限表数据时再补一条同样的语句即可不需要手动数着ID去关联。按钮权限把url设为NULL因为按钮不需要跳转路径它只负责给前端判断这个按钮该不该渲染。3. SSM工程搭建从JDK环境变量到MyBatis数据源必调参数3.1 版本选型JDK 8配Spring 5.x最稳权限管理系统属于典型的CRUD密集型应用追求的是稳定而不是新特性版本选型上不需要追新。本地环境建议JDK 8配好JAVA_HOME环境变量后在命令行执行java -version确认版本MySQL装5.7或8.0均可如果是免安装版zip解压记得在my.ini里把basedir和datadir都写成绝对路径否则启动时会报找不到数据目录。推荐版本组合如下直接照搬不会遇到版本冲突组件推荐版本选型理由JDK1.8SSM框架在JDK 8下的编译产物最成熟MySQL5.7 / 8.0归档表用utf8mb4完全够用Spring / SpringMVC5.2.x支持JDK 8无新增依赖困扰MyBatis3.5.x配合mybatis-spring 2.0.x连接池Druid 1.2.x自带监控页面便于排查连接泄漏Maven3.6依赖管理打包用3.2 Maven依赖坐标一个pom.xml拉齐SSM全家桶SSM手工搭建最大的优势就是每个依赖都知道是干什么用的。核心依赖在pom.xml里就这些注释的位置对应权限管理系统工作时的实际环节dependencies !-- Spring核心与SpringMVC -- dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version5.2.15.RELEASE/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.2.15.RELEASE/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version5.2.15.RELEASE/version /dependency !-- MyBatis与Spring整合 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.6/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.6/version /dependency !-- MySQL驱动与Druid连接池 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.28/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.8/version /dependency !-- Servlet API与JSON序列化 -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.12.3/version /dependency !-- AOP支持方法级权限注解需要 -- dependency groupIdorg.aspectj/groupId artifactIdaspectjweaver/artifactId version1.9.7/version /dependency /dependencies如果环境里Maven下载依赖很慢先在settings.xml里配置阿里云镜像这个不展开说。连依赖坐标都无法解析时优先检查本地Maven仓库是否有残留的.lastUpdated后缀文件删掉对应目录重新编译即可。3.3 数据源配置Druid三个参数影响MySQL连接稳定性SSM的Spring配置文件通常分成spring-context.xml、spring-mybatis.xml和spring-mvc.xml。权限管理系统不是超大规模并发spring-mybatis.xml里最需要关注的配置是数据源和SqlSessionFactorycontext:property-placeholder locationclasspath:jdbc.properties / bean iddataSource classcom.alibaba.druid.pool.DruidDataSource init-methodinit destroy-methodclose property namedriverClassName value${jdbc.driver} / property nameurl value${jdbc.url} / property nameusername value${jdbc.username} / property namepassword value${jdbc.password} / property nameinitialSize value5 / property nameminIdle value5 / property namemaxActive value20 / property namemaxWait value60000 / property namevalidationQuery valueSELECT 1 / property nametestWhileIdle valuetrue / property nametestOnBorrow valuefalse / property nametimeBetweenEvictionRunsMillis value60000 / /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource / property nametypeAliasesPackage valuecom.demo.permission.entity / property namemapperLocations valueclasspath:mapper/*.xml / property nameconfiguration bean classorg.apache.ibatis.session.Configuration property namemapUnderscoreToCamelCase valuetrue / /bean /property /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.demo.permission.dao / /beanjdbc.properties对应内容就是数据库连接四件套jdbc.drivercom.mysql.cj.jdbc.Driver、jdbc.urljdbc:mysql://localhost:3306/permission_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse、用户名和密码按本地环境填。serverTimezone这个参数在MySQL 8.0下必须显式指定否则驱动会报时区错误。Druid的initialSize、minIdle、maxActive是权限管理系统最常调的三个参数。initialSize5表示启动时建立5个物理连接minIdle5控制连接池最少保留5个空闲连接maxActive20是并发峰值时能达到的最大连接数。validationQuerySELECT 1是为了防止MySQL空闲超过8小时后自动断开连接Druid在归还或获取连接前先探测一下。testWhileIdle和timeBetweenEvictionRunsMillis配合让连接池每60秒扫描一次空闲连接踢掉失效连接。4. 登录认证与URL级权限拦截SSM权限系统的核心链路4.1 令牌选型内部后台优先SessionCookie权限管理系统的令牌方案在Session和JWT之间做选择。内部后台管理系统、管理端、企业OA这类场景优先用SessionCookie因为服务端能随时把某个账号踢下线改密码后旧的登录态立即失效这个特性在权限系统里比无状态重要得多。分布式部署时把Session存储切换到Redis即可不需要改架构。web.xml里配置Session超时时间单位是分钟session-config session-timeout120/session-timeout cookie-config http-onlytrue/http-only /cookie-config /session-confighttp-only这一项务必打开防止前端脚本直接读取Cookie中的会话ID。Session机制下登录成功后的标准动作就是往Session里塞用户信息和权限集合。4.2 登录Controller加盐哈希校验与状态检查登录逻辑不能只做一次密码比对。完整的流程是先按用户名查用户再判断status是否被禁用然后用MD5加盐哈希比对密码最后把用户对象和权限集合写入Session。前两步失败时返回的是“账号不存在或已禁用”这类明确提示第三步失败时统一返回“用户名或密码错误”避免暴露账号是否存在。Controller public class LoginController { Autowired private UserService userService; Autowired private PermissionService permissionService; RequestMapping(value /login, method RequestMethod.POST) ResponseBody public Result login(String username, String password, HttpSession session) { // 第一步按用户名查询用户 User user userService.findByUsername(username); if (user null || user.getStatus() ! 1) { return Result.error(账号不存在或已被禁用); } // 第二步MD5加盐哈希校验盐使用用户名 String inputPwd Md5Util.md5WithSalt(password, user.getUsername()); if (!user.getPassword().equals(inputPwd)) { return Result.error(用户名或密码错误); } // 第三步查询该用户拥有的所有权限编码 ListString perms permissionService.findPermCodesByUserId(user.getUserId()); // 第四步会话保存用户信息和权限集合 session.setAttribute(Constants.SESSION_USER, user); session.setAttribute(Constants.SESSION_PERMS, perms); return Result.success(登录成功); } }salt为什么用用户名而不是随机字符串因为登录校验密码时需要拿到同一个盐如果盐存在数据库里就要多一次查询直接用用户名做盐可以省掉一次DB访问安全性上也足够。Md5Util是把MD5(password salt)做了一轮封装的工具类不要在这个工具类里直接暴露原生MessageDigest。权限集合SESSION_PERMS在登录时一次性查出后面所有拦截器都只读Session不再碰数据库。4.3 自定义拦截器从Session读权限并匹配请求URLURL级权限拦截是这套权限管理系统最核心的部分。一个常见的错误是把校验写在Controller方法内部这样每个接口都要重复写一遍权限判断。正确做法是定义一个SpringMVC拦截器在preHandle里统一处理。public class PermissionInterceptor implements HandlerInterceptor { private ListString excludeUrls; // 放行白名单 public void setExcludeUrls(ListString excludeUrls) { this.excludeUrls excludeUrls; } Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String uri request.getRequestURI(); // 1. 白名单直接放行 if (excludeUrls ! null excludeUrls.contains(uri)) { return true; } // 2. 未登录用户统一拦截 HttpSession session request.getSession(false); if (session null || session.getAttribute(Constants.SESSION_USER) null) { response.sendRedirect(request.getContextPath() /login); return false; } // 3. 从Session获取当前用户的权限编码列表 ListString perms (ListString) session.getAttribute(Constants.SESSION_PERMS); // 4. 遍历权限集合精确或前缀匹配当前请求URI boolean hasPermission false; for (String perm : perms) { // 权限编码存的是 /user/list 这种字符串格式 if (perm.equals(uri) || uri.startsWith(perm)) { hasPermission true; break; } } if (!hasPermission) { response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.getWriter().write({\code\:403,\msg\:\无访问权限\}); return false; } return true; } }我在写权限管理系统时习惯把权限编码的规则定为“URL即权限码”也就是t_permission表的url字段放接口路径拦截器直接拿request.getRequestURI()去比。这样少一层映射维护时也直观。白名单excludeUrls在SpringMVC配置文件里声明至少放行/login、/logout、静态资源三部分。session.getSession(false)里的false表示当前没有Session时返回null而不是新建一个避免恶意请求无谓地创建会话对象。SpringMVC配置类里注册拦截器并指定白名单mvc:interceptors mvc:interceptor mvc:mapping path/** / bean classcom.demo.permission.interceptor.PermissionInterceptor property nameexcludeUrls list value/login/value value/logout/value value/static/**/value /list /property /bean /mvc:interceptor /mvc:interceptors注意/static/**这个写法是SpringMVC的Ant通配符规则/*只能匹配一层路径/**才能匹配多级目录。静态资源如果配错CSS和JS全被拦截器吃掉页面样式直接崩掉这是排查频率最高的一个坑。4.4 方法级权限校验自定义注解加切面URL拦截器解决的是“这一整个Controller路径能不能访问”的问题但权限管理系统里经常遇到同一个路径下某个操作只能特定角色执行。比如/user/delete接口管理员能调普通操作员不能调。此时就得在方法级别加上更细的权限校验。自定义注解在权限管理系统里是最常用、最容易扩展的方案Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RequiresPermission { String value(); // 权限编码如 user:delete }AOP切面配合拦截器形成“URL权限粗筛方法权限精筛”的双层控制Aspect Component public class PermissionAspect { Pointcut(annotation(com.demo.permission.annotation.RequiresPermission)) public void permissionPointcut() { } Around(permissionPointcut()) public Object checkPermission(ProceedingJoinPoint joinPoint) throws Throwable { // 获取当前请求对应的Session ServletRequestAttributes attributes (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request attributes.getRequest(); HttpSession session request.getSession(); ListString perms (ListString) session.getAttribute(Constants.SESSION_PERMS); // 获取注解上的权限编码 MethodSignature signature (MethodSignature) joinPoint.getSignature(); RequiresPermission permission signature.getMethod().getAnnotation(RequiresPermission.class); String required permission.value(); // 权限编码匹配持有的权限集里包含user:*或user:delete均可 boolean matched perms.stream() .anyMatch(p - p.equals(required) || p.endsWith(:*)); if (!matched) { throw new BusinessException(403, 无操作权限); } return joinPoint.proceed(); } }切面里的p.endsWith(:*)是权限通配符规则角色拥有user:*时表示拥有用户模块的全部操作权限。在Controller方法上使用时就三行代码RequiresPermission(user:delete) RequestMapping(/user/delete) ResponseBody public Result deleteUser(Integer userId) { userService.deleteById(userId); return Result.success(); }这套双层校验体系跑起来后外部请求进来先过拦截器判断用户是否登录、访问路径是否属于其权限范围再进AOP切面判断具体的方法调用是否匹配细粒度权限编码。测试时可以先用管理员账号登录然后手动修改Session里的权限集合列表模拟不同角色访问同一个接口的返回结果。5. 权限校验的缓存优化、五个必踩的坑与面试回答要点5.1 登录后把权限集合放入本地缓存权限集合每次请求都从Session取Session本身在内存里这步没问题。真正的性能隐患是permissionService.findPermCodesByUserId这条SQL在每次创建新Session时都会查询。用户的权限在菜单或角色变更之前是固定的完全可以用缓存扛住。单体部署时用Spring自带的Cacheable最简单Service public class PermissionService { Cacheable(cacheNames userPerms, key #userId) public ListString findPermCodesByUserId(Integer userId) { // 执行关联查询user_role - role_permission - t_permission } CacheEvict(cacheNames userPerms, key #userId) public void refreshUserPerms(Integer userId) { // 角色或权限变更后调用清除缓存 } }加缓存后必须配套缓存失效策略。管理员调整某个角色的权限后要遍历该角色下的所有用户逐个调用refreshUserPerms清缓存。如果没有清缓存用户看到的还是旧权限越权或权限残留的问题就是这么来的。分布式部署时把CacheManager换成Redis实现原理相同Key仍然是用户ID。5.2 权限管理系统常见的五个坑和对应的面试话术坑现象解法密码用明文存储数据库泄露后账号全裸MD5加盐后续升级为bcrypt角色权限修改后缓存不刷用户权限变更不生效提供缓存刷新接口业务侧调用SQL用LIKE %${param}%拼接用户管理功能存在注入风险改用CONCAT(%, #{param}, %)拦截器白名单配错登录页面死循环重定向放行login接口和静态资源权限关联表删了用户不删关联用户重新创建后带着旧角色删除用户时同步删除t_user_role关联面试时如果被问到“权限管理系统的权限控制怎么做”回答路径应该是先讲RBAC的数据库五张表设计再讲Session里存什么、拦截器怎么拦URL、AOP如何做方法级校验最后补上缓存清理策略。这套链路说下来比背八股文里的“Spring Security过滤器链”要容易让面试官听进去因为它每一步都能画出来。权限管理系统的边界也要认真回答一句RBAC管的是能不能访问数据权限管的是能看哪几行数据这两件事不混为一谈。验证整套权限配置是否正常最直接的方法是启动项目后用管理员账号登录拿Session再打开浏览器无痕窗口用普通账号登录两侧分别访问/user/list和/user/delete接口对照返回结果就能确认URL拦截和方法注解是否按预期工作。用命令行校验接口时加-b参数带上Cookie模拟已登录状态即可完整走通权限校验链路。本文还有配套的精品资源点击获取