简介这是一套基于Spring Boot、MyBatis、Spring MVC、Spring Security与Redis构建的完整网站后台管理系统源码面向具备一定Java Web基础、希望深入理解企业级开发全流程的开发者与学习者。项目将持久层、控制层、安全认证与缓存中间件协同整合可用于课程设计、毕业设计或技术栈练手帮助解决从零搭建后台时架构选型与模块集成的问题。压缩包共326个文件约1.76MB其中95个Java源文件承载核心业务与配置45个JavaScript、33个HTML与19个CSS文件构成前端交互与页面样式另有12个XML、2个YML及1个SQL文件负责映射、配置与建表并附带图片、字体等静态资源。目前已有958人学习下载。通过研读源码读者可掌握Spring Security的认证授权与CSRF防护、Redis缓存加速、MyBatis映射配置以及Spring MVC的请求处理流程理解各组件如何协同支撑一个安全、可扩展的后台系统为后续企业级项目开发积累可复用的实践经验。1. 从一份后台管理系统源码说起Spring 全家桶到底怎么串起来很多同学学完 Spring Boot 单个模块后一到真实项目就懵——Controller 里注入了 ServiceService 里又调 Mapper中间还夹着 Redis 缓存和 Spring Security 的过滤器链代码能跑但说不清数据怎么流的。这份基于 Spring Boot MyBatis Spring MVC Spring Security Redis 的完整网站后台管理系统源码恰好把这条链路完整串了一遍。它不是 Demo 级的增删改查而是带权限控制、缓存策略、分页查询的后台骨架。适合已经会写单个 CRUD、但没搭过完整分层架构的开发者也适合想对照真实项目理解 Security 过滤链和 Redis 缓存穿透处理的人。下面按「先跑起来、再拆结构、最后填坑」的顺序拆。2. 环境搭建与项目启动从零把系统跑起来2.1 依赖版本与目录结构确认拿到源码包后别急着mvn spring-boot:run先花五分钟看三样东西pom.xml里的 Spring Boot 父版本、application.yml里的数据源配置、以及src/main/java下的包结构。这份项目的典型分层是controller、service、mapper、entity、config、security、utils其中security包单独拎出来放过滤器和权限处理器说明权限逻辑不是散落在各处的。常见做法是先确认 JDK 版本。Spring Boot 2.x 配 JDK 8 或 11Spring Boot 3.x 必须 JDK 17 起步。如果pom.xml里 parent 版本是2.7.x用 JDK 8 编译最稳。MySQL 建议 5.7 或 8.0Redis 用 5.0 以上即可。这些版本不匹配是启动失败的头号原因血泪经验先对齐版本再动代码。!-- pom.xml 关键依赖片段确认这几个坐标的版本 -- parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.6/version !-- 决定 JDK 最低版本 -- /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId !-- 内含 Spring MVC -- /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.0/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency /dependencies上面这段依赖里spring-boot-starter-web已经把 Spring MVC 带进来了不需要单独引spring-webmvc。MyBatis 的 starter 版本要和 Spring Boot 版本匹配2.3.0 对应 Boot 2.7.x 是安全的。Security 和 Redis 的 starter 由 Boot 统一管理版本不写 version 也行。2.2 数据库初始化与配置文件修改源码包里一般有个sql目录里面是建表语句和初始数据。先建库再导数据顺序不能反。# 1. 登录 MySQL 并创建数据库 mysql -u root -p CREATE DATABASE admin_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; # 2. 导入表结构和初始数据假设 sql 文件在项目根目录的 sql/ 下 mysql -u root -p admin_system sql/schema.sql mysql -u root -p admin_system sql/data.sql # 3. 启动 Redis默认端口 6379无密码 redis-server建库时用utf8mb4而不是utf8因为后台系统里用户昵称、备注字段可能存 emojiutf8三字节存不下会报错。导入顺序先 schema 后 data因为 data 里的 INSERT 依赖表结构存在。接着改application.yml把数据源、Redis、MyBatis 三块配置对齐本地环境。spring: datasource: url: jdbc:mysql://localhost:3306/admin_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 database: 0 timeout: 5000ms mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.admin.entity configuration: map-underscore-to-camel-case: trueserverTimezoneAsia/Shanghai不加的话MySQL 8.0 连接会报时区错误。map-underscore-to-camel-case: true让数据库的user_name自动映射到 Java 的userName省掉大量Results注解。Redis 的database: 0表示用 0 号库如果本地还有其他项目共用 Redis改成 1 或 2 避免 key 冲突。2.3 启动验证与首个接口测试配置改完后启动项目观察控制台有没有Started Application in x seconds。如果卡在HikariPool说明数据库连不上卡在RedisConnectionFactory说明 Redis 没起。# 启动项目 mvn spring-boot:run # 另开终端测试登录接口假设登录路径是 /login curl -X POST http://localhost:8080/login \ -H Content-Type: application/json \ -d {username:admin,password:123456}如果返回一串 token 或跳转信息说明 Security 过滤链和数据库查询都通了。如果返回 403检查 Security 配置里是否放行了/login如果返回 500看控制台堆栈多半是 Mapper XML 里的 SQL 字段和实体类对不上。这一步跑通后面拆代码才有意义。3. 分层架构拆解Controller、Service、Mapper 各管什么3.1 请求从进入到返回的完整链路一个后台管理系统的典型请求是「查用户列表」。请求先到 Spring Security 的过滤器链经过认证和授权判断通过后进入 DispatcherServlet根据 URL 找到对应的 Controller 方法。Controller 不写业务逻辑只做参数接收和结果封装然后调 Service。Service 里做业务判断、缓存读写、事务控制再调 Mapper。Mapper 是接口真正的 SQL 在 XML 里查完数据库返回实体逐层往上传递。这条链路里最容易出问题的是 Security 过滤器和 Controller 的衔接。比如登录接口需要匿名访问但其他接口需要认证配置写错就会出现「登录接口也被拦截」的死循环。常见做法是在 Security 配置类里用antMatchers(/login, /captcha).permitAll()显式放行其余anyRequest().authenticated()。// SecurityConfig 关键片段放行登录和验证码其余需认证 Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() // 前后端分离项目通常关掉 csrf .authorizeRequests() .antMatchers(/login, /captcha, /static/**).permitAll() .anyRequest().authenticated() .and() .formLogin().loginProcessingUrl(/login).permitAll() .and() .logout().logoutUrl(/logout).permitAll(); }csrf().disable()在纯 API 项目里是常规操作因为前端用 token 而不是 session cookie。loginProcessingUrl(/login)告诉 Security 哪个路径处理登录表单提交和 Controller 里的PostMapping(/login)要区分开——前者是 Security 自己处理的后者是你自己写的业务登录。两者混用是新手翻车高发区。3.2 MyBatis 映射与分页查询实现后台系统离不开分页。这份源码里分页通常用 PageHelper 插件在 Service 里调PageHelper.startPage(pageNum, pageSize)紧跟着的 Mapper 查询会自动拼上LIMIT。// UserServiceImpl.java 分页查询用户 public PageInfoUser listUsers(int pageNum, int pageSize, String keyword) { PageHelper.startPage(pageNum, pageSize); // 只对下一条查询生效 ListUser users userMapper.selectByKeyword(keyword); return new PageInfo(users); // 封装总页数、总记录数等 }PageHelper.startPage必须紧挨着查询语句中间不能插入其他数据库操作否则分页会作用到错误的 SQL 上。PageInfo会自动计算total、pages、hasNextPage等字段前端直接拿这些字段渲染分页组件。如果返回的total是 0 但数据明明存在检查 Mapper XML 里有没有写resultMap且字段映射正确。对应的 Mapper XML 里SQL 不需要自己写LIMITPageHelper 会拦截并改写。!-- UserMapper.xml -- select idselectByKeyword resultMapBaseResultMap SELECT id, user_name, nick_name, status, create_time FROM sys_user where if testkeyword ! null and keyword ! AND (user_name LIKE CONCAT(%, #{keyword}, %) OR nick_name LIKE CONCAT(%, #{keyword}, %)) /if /where ORDER BY create_time DESC /selectwhere标签会自动处理第一个AND避免WHERE AND的语法错误。CONCAT(%, #{keyword}, %)用#{}而不是${}前者是预编译占位符能防 SQL 注入后者是字符串拼接有注入风险。这个细节在后台系统里尤其重要因为搜索框是用户直接输入的入口。3.3 Redis 缓存层接入与 key 设计后台系统里频繁查询但很少变动的数据适合放 Redis比如菜单树、字典项、用户权限列表。接入方式一般是在 Service 方法上加Cacheable或者手动用RedisTemplate读写。// 手动缓存示例先查 Redis没有再查库并回写 public ListMenu getMenuTree() { String key admin:menu:tree; ListMenu cached (ListMenu) redisTemplate.opsForValue().get(key); if (cached ! null) { return cached; // 命中缓存直接返回 } ListMenu menus menuMapper.selectAll(); redisTemplate.opsForValue().set(key, menus, 30, TimeUnit.MINUTES); // 30 分钟过期 return menus; }key 的命名建议用业务:模块:标识的格式比如admin:menu:tree、admin:user:token:{userId}。这样在 Redis 客户端里能按前缀批量查看和清理。过期时间一定要设后台系统的菜单数据虽然变动少但不设过期会导致内存持续增长。30 分钟是个折中值既能减少数据库压力又不至于数据太旧。注意Redis 里存的对象需要可序列化。如果实体类没实现Serializable或者RedisTemplate没配置 JSON 序列化器存进去再取出来会报类型转换异常。常见做法是给RedisTemplate配Jackson2JsonRedisSerializer。4. 权限控制与缓存踩坑排查4.1 Spring Security 过滤链不生效的三种情况现象一所有接口都能匿名访问Security 形同虚设。原因通常是配置类没有加EnableWebSecurity或者继承了WebSecurityConfigurerAdapter但没重写configure方法。解决方式是检查配置类注解是否齐全方法签名是否和父类一致。现象二登录后访问其他接口仍然 403。原因多半是权限角色没对上。比如数据库里用户角色是ROLE_ADMIN但配置里写的是hasRole(ADMIN)——hasRole会自动加ROLE_前缀所以配置里不要重复写。如果数据库存的是ADMIN不带前缀就用hasAuthority(ADMIN)。现象三静态资源被拦截导致页面样式丢失。原因是 Security 默认拦截所有路径CSS、JS、图片请求也被要求认证。解决方式是在配置里放行/static/**、/css/**、/js/**等路径或者重写configure(WebSecurity web)方法忽略静态资源。4.2 Redis 缓存与数据库不一致的排查现象更新了用户信息但页面刷新后还是旧数据。原因是缓存没清。更新数据库后没有删除对应的 Redis key下次查询直接命中旧缓存。解决方式是在 Service 的更新方法里数据库操作成功后立即redisTemplate.delete(key)或者用CacheEvict注解。现象Redis 里 key 越来越多内存报警。原因是只写不删且没设过期时间。排查方式是登录 Redis 客户端执行KEYS admin:*看有多少业务 key再用TTL key看剩余时间。如果返回-1说明永不过期需要补上过期策略。现象高并发下缓存击穿数据库压力骤增。原因是某个热点 key 过期瞬间大量请求同时打到数据库。常见做法是加互斥锁只让一个线程去查库回写其他线程短暂等待后重试读缓存。4.3 MyBatis 映射报错的定位方法现象启动时报Invalid bound statement (not found)。原因是 Mapper 接口和 XML 没对上。检查三处XML 的namespace是否等于 Mapper 接口全限定名XML 里id是否等于接口方法名application.yml里mapper-locations路径是否能扫到 XML 文件。现象查询返回的实体字段全是 null。原因是数据库列名和 Java 属性名没映射上。如果开了map-underscore-to-camel-caseuser_name能自动映射到userName如果没开需要在 XML 里写resultMap手动映射或者给 SQL 字段起别名SELECT user_name AS userName。现象插入数据后主键回填不到实体里。原因是 insert 标签没配useGeneratedKeys和keyProperty。正确写法是insert idinsert useGeneratedKeystrue keyPropertyid这样插入后实体的 id 字段会自动填充数据库生成的主键值。5. 二次开发与接口验证的实用技巧5.1 新增一个业务模块的最小改动清单在现有后台系统上加一个新模块比如「公告管理」改动集中在五个文件建表 SQL、实体类、Mapper 接口、Mapper XML、Service 和 Controller。按这个顺序写每写完一层就编译一次能快速定位错误在哪层。// 1. 实体类 Notice.java Data public class Notice implements Serializable { private Long id; private String title; private String content; private Date createTime; } // 2. Mapper 接口 NoticeMapper.java Mapper public interface NoticeMapper { ListNotice selectAll(); int insert(Notice notice); int deleteById(Long id); } // 3. Service 层调用 MapperController 层暴露 REST 接口 RestController RequestMapping(/notice) public class NoticeController { Autowired private NoticeService noticeService; GetMapping(/list) public Result list() { return Result.success(noticeService.listAll()); } }实体类实现Serializable是为了后续可能放入 Redis 缓存。Mapper注解让 MyBatis 扫描到这个接口如果启动类上已经加了MapperScan(com.example.admin.mapper)接口上的Mapper可以省略。Controller 返回统一的Result封装类前端处理起来更规范。5.2 用 Postman 或 curl 验证接口的检查顺序调接口报错时按「请求是否到达 Controller → 参数是否绑定成功 → Service 逻辑是否正确 → SQL 是否执行」的顺序排查不要一上来就改代码。# 带 token 访问受保护接口 curl -X GET http://localhost:8080/notice/list \ -H Authorization: Bearer 你的token # 如果返回 401说明 token 无效或没传 # 如果返回 403说明 token 有效但权限不够 # 如果返回 200 但数据为空检查数据库里有没有数据先在 Controller 方法第一行打日志确认请求进来了再在 Service 入口打日志确认参数传对了最后在 Mapper 对应的 SQL 上开 MyBatis 的log-impl打印实际执行的 SQL。这三层日志一开问题基本无处遁形。5.3 从源码里学到的三个工程习惯第一个习惯是配置文件分环境。application-dev.yml、application-prod.yml分开主配置文件里用spring.profiles.active切换避免把本地数据库密码提交到仓库。第二个习惯是统一异常处理。用RestControllerAdvice捕获全局异常返回统一的错误码和提示而不是让 500 堆栈直接暴露给前端。第三个习惯是接口返回统一封装。ResultT包含code、msg、data三个字段前端只需判断code就能知道成功还是失败。从那以后我每次拿到一份新源码都强制先跑通登录接口再拆代码因为登录链路串起了数据库、Security、Redis 和 MVC 四层它通了整个项目的骨架就立住了。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?