简介:这是一套基于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 里注入了 Service,Service 里又调 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 或 11,Spring Boot 3.x 必须 JDK 17 起步。如果pom.xml里 parent 版本是2.7.x,用 JDK 8 编译最稳。MySQL 建议 5.7 或 8.0,Redis 用 5.0 以上即可。这些版本不匹配是启动失败的头号原因,血泪经验:先对齐版本再动代码。
<!-- pom.xml 关键依赖片段,确认这几个坐标的版本 --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.6</version> <!-- 决定 JDK 最低版本 --> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <!-- 内含 Spring MVC --> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-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,因为后台系统里用户昵称、备注字段可能存 emoji,utf8三字节存不下会报错。导入顺序先 schema 后 data,因为 data 里的 INSERT 依赖表结构存在。
接着改application.yml,把数据源、Redis、MyBatis 三块配置对齐本地环境。
spring: datasource: url: jdbc:mysql://localhost:3306/admin_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/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: trueserverTimezone=Asia/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 PageInfo<User> listUsers(int pageNum, int pageSize, String keyword) { PageHelper.startPage(pageNum, pageSize); // 只对下一条查询生效 List<User> users = userMapper.selectByKeyword(keyword); return new PageInfo<>(users); // 封装总页数、总记录数等 }PageHelper.startPage必须紧挨着查询语句,中间不能插入其他数据库操作,否则分页会作用到错误的 SQL 上。PageInfo会自动计算total、pages、hasNextPage等字段,前端直接拿这些字段渲染分页组件。如果返回的total是 0 但数据明明存在,检查 Mapper XML 里有没有写resultMap且字段映射正确。
对应的 Mapper XML 里,SQL 不需要自己写LIMIT,PageHelper 会拦截并改写。
<!-- UserMapper.xml --> <select id="selectByKeyword" resultMap="BaseResultMap"> SELECT id, user_name, nick_name, status, create_time FROM sys_user <where> <if test="keyword != null and keyword != ''"> AND (user_name LIKE CONCAT('%', #{keyword}, '%') OR nick_name LIKE CONCAT('%', #{keyword}, '%')) </if> </where> ORDER BY create_time DESC </select><where>标签会自动处理第一个AND,避免WHERE AND的语法错误。CONCAT('%', #{keyword}, '%')用#{}而不是${},前者是预编译占位符能防 SQL 注入,后者是字符串拼接有注入风险。这个细节在后台系统里尤其重要,因为搜索框是用户直接输入的入口。
3.3 Redis 缓存层接入与 key 设计
后台系统里频繁查询但很少变动的数据适合放 Redis,比如菜单树、字典项、用户权限列表。接入方式一般是在 Service 方法上加@Cacheable,或者手动用RedisTemplate读写。
// 手动缓存示例:先查 Redis,没有再查库并回写 public List<Menu> getMenuTree() { String key = "admin:menu:tree"; List<Menu> cached = (List<Menu>) redisTemplate.opsForValue().get(key); if (cached != null) { return cached; // 命中缓存直接返回 } List<Menu> 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-case,user_name能自动映射到userName;如果没开,需要在 XML 里写resultMap手动映射,或者给 SQL 字段起别名SELECT user_name AS userName。
现象:插入数据后主键回填不到实体里。原因是 insert 标签没配useGeneratedKeys和keyProperty。正确写法是<insert id="insert" useGeneratedKeys="true" keyProperty="id">,这样插入后实体的 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 { List<Notice> selectAll(); int insert(Notice notice); int deleteById(Long id); } // 3. Service 层调用 Mapper,Controller 层暴露 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 堆栈直接暴露给前端。第三个习惯是接口返回统一封装。Result<T>包含code、msg、data三个字段,前端只需判断code就能知道成功还是失败。
从那以后我每次拿到一份新源码,都强制先跑通登录接口再拆代码,因为登录链路串起了数据库、Security、Redis 和 MVC 四层,它通了,整个项目的骨架就立住了。希望帮到你。
本文还有配套的精品资源,点击获取