☰
SpringBoot+SpringSecurity+Redis后台管理系统源码实战:权限认证与缓存设计
2026/10/23 6:45:25 网站建设 项目流程

简介:这是一套面向Java后端开发者与计算机专业学生的完整网站后台管理系统源码,基于Spring Boot、MyBatis、Spring MVC、Spring Security与Redis构建,适合希望掌握企业级Web开发全链路技术的学习者。项目将持久层、控制层、安全认证与缓存中间件协同整合,可用于课程设计、毕业设计或技术栈实战演练。压缩包共326个文件,约1.76MB,其中95个Java源文件承载核心业务与配置,45个JavaScript、33个HTML与19个CSS文件构成前端交互与页面样式,另有12个XML映射文件、2个YML配置及1个SQL脚本支撑数据层与运行环境,并附带GIF、PNG等静态素材。目前已有958人学习下载。通过研读源码,读者可理解Spring Security的登录验证与权限控制流程、MyBatis的SQL映射方式、Redis缓存集成思路以及Spring Boot的自动配置机制,进而掌握现代Java Web项目从架构分层到安全防护的完整实现路径。

1. 从一份后台管理系统源码说起:为什么我建议你亲手跑一遍

很多开发者拿到后台管理系统源码的第一反应是“这不就是增删改查吗”,然后扔进收藏夹吃灰。但这份基于 SpringBoot + MyBatis + SpringMVC + SpringSecurity + Redis 的完整网站后台管理系统,恰恰是那种“看着简单、拆开全是细节”的项目。它把权限控制、缓存策略、前后端交互、数据库映射这几块最容易翻车的地方全串起来了。如果你正在找一个能覆盖企业级后台核心链路的练手项目,或者想搞明白 SpringSecurity 和 Redis 在真实项目里怎么配合,这份源码值得你花一个周末跑通并拆解。它适合有 Java Web 基础、想从“会写接口”进阶到“能搭系统”的开发者。

2. 技术栈选型与整体架构:为什么是这五件套

2.1 分层架构与请求链路

这个项目采用的是典型的 MVC 分层结构,但比教科书上的示例多了两层实际考量:安全层和缓存层。一个请求从浏览器发出后,先经过 SpringSecurity 的过滤器链做认证和授权判断,通过后再进入 SpringMVC 的 DispatcherServlet 做路由分发,Controller 调用 Service,Service 通过 MyBatis 的 Mapper 操作数据库,期间热点数据会走 Redis 缓存。

常见做法是把 Security 的过滤器链配置在 WebSecurityConfigurerAdapter 的子类里,用http.authorizeRequests()定义 URL 级别的权限规则。这里有个容易忽略的点:SpringSecurity 的过滤器执行顺序在 SpringMVC 之前,所以你在 Controller 里拿到的 SecurityContext 已经是认证过的,不需要重复校验。

@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() // 静态资源和登录页放行 .antMatchers("/login", "/css/**", "/js/**").permitAll() // 管理接口需要 ADMIN 角色 .antMatchers("/admin/**").hasRole("ADMIN") // 其余请求登录后访问 .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") .defaultSuccessUrl("/index") .and() .logout() .logoutSuccessUrl("/login"); } }

这段配置定义了三个关键规则:放行静态资源、限制管理接口角色、其余请求需认证。hasRole("ADMIN")会自动加上ROLE_前缀,所以数据库里存的角色名应该是ROLE_ADMIN或者你在 UserDetailsService 里手动拼接。很多新手在这里踩坑,数据库存了ADMIN,配置写hasRole("ADMIN"),结果一直 403,原因就是前缀没对上。

2.2 目录结构与模块划分

项目目录遵循 Maven 标准结构,但包名划分能看出作者对职责边界的理解。常见划分方式是:

src/main/java/com/example/admin/ ├── config/ # Security、Redis、MyBatis 配置类 ├── controller/ # 接口层,按业务模块分子包 ├── service/ # 业务逻辑,接口与实现分离 ├── mapper/ # MyBatis Mapper 接口 ├── entity/ # 数据库实体 ├── dto/ # 数据传输对象 └── util/ # 工具类,如 RedisUtil、ResultUtil

config包集中放配置类,方便统一管理。dto包的存在说明作者考虑到了前后端字段差异,比如用户实体里有密码字段,但返回给前端时要剔除,这时候就用 DTO 做转换。util包里的 RedisUtil 通常封装了set、get、expire等常用操作,避免在 Service 里直接写 RedisTemplate 的样板代码。

2.3 Redis 在项目中的三个实际用途

Redis 在这个项目里不是摆设,它承担了三个具体职责:缓存登录用户信息、存储会话 Token、缓存高频查询结果。登录成功后,用户信息会以user:token:{token}为 key 写入 Redis,并设置过期时间。后续请求携带 Token,SpringSecurity 的过滤器从 Redis 读取用户信息完成认证,而不是每次查数据库。

@Service public class UserServiceImpl implements UserService { @Autowired private RedisTemplate<String, Object> redisTemplate; private static final String TOKEN_PREFIX = "user:token:"; private static final long TOKEN_EXPIRE = 30; // 分钟 @Override public String login(String username, String password) { User user = userMapper.selectByUsername(username); if (user == null || !passwordEncoder.matches(password, user.getPassword())) { throw new BusinessException("用户名或密码错误"); } String token = UUID.randomUUID().toString(); // 写入 Redis,过期时间 30 分钟 redisTemplate.opsForValue().set( TOKEN_PREFIX + token, user, TOKEN_EXPIRE, TimeUnit.MINUTES); return token; } }

这里用UUID生成 Token 而不是 JWT,好处是服务端可以主动失效 Token,坏处是需要 Redis 做存储。TOKEN_EXPIRE设为 30 分钟是常见做法,实际项目中可以根据安全要求调整。注意redisTemplate的序列化方式,默认的 JDK 序列化在 Redis 里存的是二进制,用命令行看会乱码,建议配置成 Jackson 或 String 序列化。

3. 环境搭建与启动步骤:从零到跑通

3.1 依赖版本与数据库准备

项目基于 SpringBoot 2.x,JDK 要求 1.8 以上。Maven 依赖里除了 spring-boot-starter-web、mybatis-spring-boot-starter、spring-boot-starter-security、spring-boot-starter-data-redis 这几个核心 starter,还需要 mysql-connector-java 和 druid 连接池。常见做法是在pom.xml里用<parent>继承 spring-boot-starter-parent,统一管理版本号。

数据库需要提前建好,执行项目resources/sql目录下的初始化脚本。脚本通常包含建表语句和初始数据,比如sys_user、sys_role、sys_permission三张核心表。注意字符集用utf8mb4,否则中文和特殊符号会出问题。

CREATE TABLE `sys_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT '加密后的密码', `status` tinyint(1) DEFAULT '1' COMMENT '1启用 0禁用', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

password字段长度给到 100 是因为 BCrypt 加密后的字符串长度在 60 左右,留足余量。status字段用于软禁用用户,比直接删除更安全。

3.2 配置文件关键项

application.yml里需要改三个地方:数据库连接、Redis 连接、MyBatis 映射路径。数据库密码和 Redis 密码不要硬编码在 yml 里,常见做法是用环境变量或者配置中心,本地开发可以先用明文,但提交代码前记得改成占位符。

spring: datasource: url: jdbc:mysql://localhost:3306/admin_db?useUnicode=true&characterEncoding=utf8 username: root password: ${DB_PASSWORD:123456} driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 password: ${REDIS_PASSWORD:} database: 0 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.admin.entity

${DB_PASSWORD:123456}的写法表示优先读环境变量,读不到就用默认值。mapper-locations指向 XML 映射文件位置,如果 Mapper 接口和 XML 放在同一包下且同名,这个配置可以省略,但显式写出来更清晰。

3.3 启动与验证

启动类上需要加@MapperScan("com.example.admin.mapper")注解,否则 MyBatis 找不到 Mapper 接口。启动成功后访问http://localhost:8080/login,用初始化脚本里的管理员账号登录。默认账号通常是admin,密码在 SQL 脚本里可能是明文或者已经加密的字符串。

验证 Redis 是否生效,可以在登录后执行redis-cli keys "user:token:*",应该能看到一条记录。如果 Redis 连接失败,检查application.yml里的 host 和 port,以及 Redis 服务是否设置了密码。SpringBoot 2.x 默认使用 Lettuce 客户端,如果项目里引入了 Jedis,需要排除 Lettuce 依赖,否则会冲突。

4. 核心模块拆解:权限、缓存与数据映射

4.1 SpringSecurity 认证流程与自定义 UserDetailsService

SpringSecurity 的认证入口是UsernamePasswordAuthenticationFilter,它从表单里取 username 和 password,封装成UsernamePasswordAuthenticationToken,然后交给AuthenticationManager处理。AuthenticationManager会调用你配置的UserDetailsService来加载用户信息。

@Service public class CustomUserDetailsService implements UserDetailsService { @Autowired private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userMapper.selectByUsername(username); if (user == null) { throw new UsernameNotFoundException("用户不存在"); } // 查询用户角色和权限 List<String> permissions = userMapper.selectPermissionsByUserId(user.getId()); return new org.springframework.security.core.userdetails.User( user.getUsername(), user.getPassword(), user.getStatus() == 1, true, true, true, permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList()) ); } }

loadUserByUsername返回的UserDetails对象里包含了密码和权限列表。注意第三个参数enabled传的是user.getStatus() == 1,这样被禁用的用户会在认证阶段就被拦截。权限列表用SimpleGrantedAuthority包装,如果数据库里存的是ROLE_ADMIN,这里直接传即可;如果存的是ADMIN,需要手动拼ROLE_前缀。

4.2 MyBatis 映射与动态 SQL

MyBatis 的 XML 映射文件里,动态 SQL 是高频使用的特性。比如用户列表查询,需要根据用户名、状态、角色等多个条件组合筛选,用<if>标签拼接 WHERE 子句。

<select id="selectByCondition" resultType="com.example.admin.entity.User"> SELECT * FROM sys_user <where> <if test="username != null and username != ''"> AND username LIKE CONCAT('%', #{username}, '%') </if> <if test="status != null"> AND status = #{status} </if> <if test="roleId != null"> AND id IN (SELECT user_id FROM sys_user_role WHERE role_id = #{roleId}) </if> </where> ORDER BY id DESC </select>

<where>标签会自动处理第一个AND,避免WHERE AND的语法错误。CONCAT('%', #{username}, '%')是 MySQL 的模糊查询写法,用#{}而不是${}是为了防止 SQL 注入。如果项目里用了 PageHelper 分页插件,在查询前调用PageHelper.startPage(pageNum, pageSize)即可,MyBatis 会自动在 SQL 后面加 LIMIT。

4.3 Redis 缓存穿透与雪崩的简单防护

项目里对高频查询做了缓存,但缓存穿透和雪崩是绕不开的问题。缓存穿透指查询一个数据库里也不存在的 key,导致每次请求都打到数据库。常见做法是缓存空值并设置较短的过期时间。

public User getUserById(Long id) { String cacheKey = "user:id:" + id; Object cached = redisTemplate.opsForValue().get(cacheKey); if (cached != null) { // 如果是空值占位符,直接返回 null if (cached instanceof NullValue) { return null; } return (User) cached; } User user = userMapper.selectById(id); if (user == null) { // 缓存空值,过期时间 5 分钟,防止穿透 redisTemplate.opsForValue().set(cacheKey, new NullValue(), 5, TimeUnit.MINUTES); return null; } redisTemplate.opsForValue().set(cacheKey, user, 30, TimeUnit.MINUTES); return user; }

NullValue是一个自定义的空对象标记类,用来区分“缓存里没有”和“缓存里存的是空值”。缓存雪崩的防护则是给过期时间加随机值,比如30 + RandomUtils.nextInt(5)分钟,避免大量 key 同时失效。

5. 避坑与常见问题排查

5.1 登录后跳转 403 但控制台无报错

现象是输入正确账号密码后,页面跳转到 403 页面,后端日志没有异常。原因通常是 SpringSecurity 的 CSRF 防护拦截了 POST 请求,而登录表单没有携带 CSRF Token。解决方式有两种:一是在表单里加<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>,二是在 Security 配置里对特定接口关闭 CSRF,比如.csrf().ignoringAntMatchers("/api/**")。前后端分离项目通常选第二种。

5.2 Redis 里存的对象取出来变成 LinkedHashMap

现象是从 Redis 读取用户对象后,强转成 User 类型抛出 ClassCastException,提示 LinkedHashMap 无法转换为 User。原因是 RedisTemplate 默认使用 JDK 序列化,但如果你配置了 Jackson 序列化却没有指定类型信息,Jackson 会把对象反序列化成 LinkedHashMap。解决方式是在 RedisConfig 里配置Jackson2JsonRedisSerializer并激活默认类型信息,或者直接用GenericJackson2JsonRedisSerializer。

5.3 MyBatis 查询返回字段为 null 但数据库有值

现象是 SQL 在数据库客户端执行正常,但 MyBatis 映射出来的对象字段全是 null。原因通常是实体类字段名和数据库列名不一致,且没有开启驼峰映射。比如数据库列user_name,实体字段userName,需要在application.yml里加mybatis.configuration.map-underscore-to-camel-case: true。如果字段名差异较大,需要在 XML 里用<resultMap>手动映射。

5.4 静态资源被 SpringSecurity 拦截

现象是登录页的 CSS 和 JS 加载失败,浏览器控制台报 302 或 403。原因是 Security 配置里没有放行静态资源路径。解决方式是在authorizeRequests()里加上.antMatchers("/css/**", "/js/**", "/images/**").permitAll()。注意 SpringBoot 默认的静态资源目录是resources/static,如果放在resources/public或resources/webjars,路径规则要相应调整。

5.5 数据库连接池启动时报 Communications link failure

现象是项目启动时 Druid 连接池初始化失败,报Communications link failure。原因通常是 MySQL 8.x 的驱动类名和 URL 参数变了。驱动类要用com.mysql.cj.jdbc.Driver,URL 要加serverTimezone=Asia/Shanghai,否则会报时区错误。另外 MySQL 8 默认的认证插件是caching_sha2_password,如果驱动版本过低会连接失败,升级 mysql-connector-java 到 8.0 以上即可。

6. 二次开发改造:从能跑到好用

6.1 把 Token 认证改成 JWT 无状态方案

当前项目用 Redis 存储 Token,服务端可以主动失效,但每次请求都要查 Redis。如果要做水平扩展,可以改成 JWT,把用户信息和过期时间编码进 Token 本身,服务端只做签名校验。改造步骤:引入jjwt依赖,写一个JwtUtil工具类,在登录成功后生成 JWT 返回给前端,在 Security 过滤器里解析 JWT 并构建Authentication对象。注意 JWT 的密钥要足够复杂,且不要放在代码里硬编码。

public class JwtUtil { private static final String SECRET = "your-256-bit-secret"; private static final long EXPIRE = 30 * 60 * 1000; // 30分钟 public static String generateToken(String username, List<String> roles) { return Jwts.builder() .setSubject(username) .claim("roles", roles) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + EXPIRE)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); } }

改造后 Redis 只用来做黑名单(登出时把未过期的 JWT 加入黑名单),不再存储用户信息。这样服务端可以做到无状态,适合容器化部署。

6.2 增加操作日志与审计功能

后台管理系统通常需要记录谁在什么时间做了什么操作。可以用 SpringAOP 加自定义注解实现,在 Controller 方法上标注@LogOperation("删除用户"),切面里拦截注解,把操作人、操作时间、请求参数、返回结果写入sys_operation_log表。注意请求参数里的密码字段要脱敏,否则日志里会泄露敏感信息。

6.3 接口权限细化到按钮级别

当前项目的权限控制是 URL 级别的,但实际业务中同一个页面里不同按钮的权限可能不同。改造方式是在sys_permission表里增加type字段区分菜单和按钮,前端根据权限列表控制按钮的显示隐藏,后端在 Controller 方法上加@PreAuthorize("hasAuthority('user:delete')")做细粒度校验。这样即使前端被绕过,后端依然能拦住越权操作。

6.4 用 Docker Compose 一键拉起依赖服务

每次换电脑都要装 MySQL 和 Redis 很麻烦,写一个docker-compose.yml把依赖服务容器化,改配置时只改环境变量。

version: '3.8' services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: admin_db ports: - "3306:3306" volumes: - ./sql:/docker-entrypoint-initdb.d redis: image: redis:6.2 ports: - "6379:6379"

把初始化 SQL 脚本放在./sql目录下,MySQL 容器启动时会自动执行。这样新同事拉下代码后只需要docker-compose up -d,再启动 SpringBoot 应用就能跑通,省去了装环境和导数据的沟通成本。

6.5 一个我踩过的坑:缓存和数据库的一致性

有次改用户角色后,前端刷新页面还是旧权限,查了半天发现是 Redis 里的用户缓存没更新。后来我定了一个规矩:任何涉及用户、角色、权限的写操作,执行完数据库更新后,立刻删除对应的 Redis 缓存 key,而不是更新缓存。删除比更新更安全,因为更新可能因为并发导致旧值覆盖新值。从那以后我每次写缓存相关的代码,都强制走一遍“先更新数据库,再删除缓存”的流程,再也没出现过权限不一致的问题。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询