🧱 第一步:环境准备
✅ 1. 创建数据库(MySQL)
-- 创建数据库,使用 utf8mb4 字符集支持 emoji 和多语言
CREATE DATABASE security_demo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;-- 使用该数据库
USE security_demo;-- 用户表结构
-- id: 主键自增
-- username: 唯一,不允许重复
-- password: 存储 BCrypt 加密后的密码(明文不可逆)
-- role: 存储用户角色,如 ROLE_ADMIN、ROLE_USER
CREATE TABLE user (id BIGINT PRIMARY KEY AUTO_INCREMENT,username VARCHAR(50) UNIQUE NOT NULL,password VARCHAR(100) NOT NULL, -- BCrypt 加密后长度约 60role VARCHAR(50) NOT NULL
);-- 插入测试数据
-- 注意:密码 '123456' 已通过 BCrypt 加密(强度为 10)
-- 生成工具:https://www.devglan.com/online-tools/bcrypt-hash-generator
INSERT INTO user (username, password, role) VALUES
('admin', '$2a$10$RRLCewx/5eYR60ZJ6y6U7eM8V6a8y6U7eM8V6a8y6U7eM8V6a8y6U7', 'ROLE_ADMIN'),
('alice', '$2a$10$RRLCewx/5eYR60ZJ6y6U7eM8V6a8y6U7eM8V6a8y6U7eM8V6a8y6U7', 'ROLE_USER'),
('bob', '$2a$10$RRLCewx/5eYR60ZJ6y6U7eM8V6a8y6U7eM8V6a8y6U7eM8V6a8y6U7', 'ROLE_USER');
🔐 安全提示:
- 不要将明文密码存入数据库。
BCrypt
是 Spring Security 推荐的密码加密算法,自带盐值(salt),防彩虹表攻击。$2a$10$...
中的10
是加密强度(log rounds),值越大越安全但越慢。
✅ 2. Redis
# 确保 Redis 正在运行
redis-server
💡 用途说明:
- 缓存用户角色信息,避免每次请求都查询数据库。
- 提升系统性能,尤其在高并发场景下。
- 键名格式:
user_role:用户名
📁 第二步:Spring Boot 项目结构
src/main/java/com/example/demo/
├── DemoApplication.java # 主启动类
├── config/
│ ├── SecurityConfig.java # 安全核心配置
│ └── MyBatisConfig.java # MyBatis 配置(可选)
├── controller/
│ └── UserController.java # 用户操作接口
├── entity/
│ └── User.java # 用户实体类
├── mapper/
│ └── UserMapper.java # 数据访问接口
├── service/
│ ├── CustomUserDetailsService.java # 自定义用户认证逻辑
│ └── RedisService.java # Redis 操作封装
└── util/└── PasswordUtil.java # 密码工具类(未使用,建议补全)
📦 pom.xml
依赖详解
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.3.3</version> <!-- 使用最新稳定版 Spring Boot --><relativePath/> <!-- 查找父 POM 从本地开始 --></parent><groupId>com.example</groupId><artifactId>security-demo</artifactId><version>0.0.1-SNAPSHOT</version><name>security-demo</name><properties><java.version>17</java.version> <!-- 推荐使用 LTS 版本 --></properties><dependencies><!-- Web 支持:Tomcat + Spring MVC --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- 安全框架:Spring Security --><!-- 提供认证、授权、CSRF、Session 等功能 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!-- MyBatis 启动器 --><!-- 简化 MyBatis 配置,自动扫描 Mapper --><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>3.0.3</version></dependency><!-- MySQL 驱动 --><!-- 运行时依赖,编译时不需要 --><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope></dependency><!-- Redis 支持 --><!-- 用于缓存用户权限信息 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!-- Lombok --><!-- 自动生成 getter/setter/toString 等方法 --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><!-- 打包时排除 Lombok,避免运行时报错 --><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes></configuration></plugin></plugins></build>
</project>
✅ 第三步:代码实现
1. DemoApplication.java
- 主启动类
package com.example.demo;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;/*** Spring Boot 主启动类* @SpringBootApplication 注解 = @Configuration + @EnableAutoConfiguration + @ComponentScan* 自动扫描 com.example.demo 包下所有组件*/
@SpringBootApplication
public class DemoApplication {public static void main(String[] args) {SpringApplication.run(DemoApplication.class, args);}
}
2. User.java
- 实体类
package com.example.demo.entity;import lombok.Data;/*** 用户实体类* 对应数据库 user 表* 使用 Lombok @Data 自动生成:* - getter/setter* - toString()* - equals()/hashCode()* - requiredArgsConstructor*/
@Data
public class User {private Long id;private String username;private String password;private String role; // 如 ROLE_ADMIN
}
3. UserMapper.java
- MyBatis Mapper
package com.example.demo.mapper;import com.example.demo.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;/*** 数据访问接口(DAO)* @Mapper 注解:让 Spring 能扫描到该接口并创建代理对象* SQL 注解方式:@Select 直接写 SQL,适合简单查询*/
@Mapper
public interface UserMapper {/*** 根据用户名查询用户信息* #{username} 是预编译参数,防止 SQL 注入* @param username 用户名* @return 用户对象,不存在返回 null*/@Select("SELECT * FROM user WHERE username = #{username}")User findByUsername(String username);
}
4. RedisService.java
- Redis 工具类
package com.example.demo.service;import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;import java.util.concurrent.TimeUnit;/*** Redis 操作服务类* 封装常用操作,便于业务调用* 使用 StringRedisTemplate(只处理字符串),适合缓存简单键值对*/
@Service
public class RedisService {private final StringRedisTemplate redisTemplate;/*** 构造器注入 RedisTemplate* Spring 自动注入 RedisConnectionFactory 创建的模板*/public RedisService(StringRedisTemplate redisTemplate) {this.redisTemplate = redisTemplate;}/*** 设置字符串值,并设置过期时间(分钟)* @param key 键* @param value 值* @param timeout 过期时间(分钟)*/public void set(String key, String value, long timeout) {redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.MINUTES);}/*** 获取字符串值* @param key 键* @return 值,不存在返回 null*/public String get(String key) {return redisTemplate.opsForValue().get(key);}/*** 删除指定键* @param key 键*/public void delete(String key) {redisTemplate.delete(key);}
}
5. CustomUserDetailsService.java
- 自定义用户详情服务
package com.example.demo.service;import com.example.demo.entity.User;
import com.example.demo.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;import java.util.Collection;
import java.util.Collections;/*** 自定义用户详情服务* Spring Security 通过此服务加载用户信息用于认证* 实现 UserDetailsService 接口是必须的*/
@Service
public class CustomUserDetailsService implements UserDetailsService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate RedisService redisService;/*** 根据用户名加载用户详情* 调用时机:用户登录时(/login)* 流程:* 1. 先查 Redis 缓存* 2. 缓存命中 → 返回* 3. 未命中 → 查数据库 → 写入缓存* @param username 用户名* @return UserDetails(Spring Security 用户模型)* @throws UsernameNotFoundException 用户不存在时抛出*/@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 1. 尝试从 Redis 获取角色String cachedRole = redisService.get("user_role:" + username);if (cachedRole != null) {System.out.println("✅ Redis 缓存命中: " + username);return buildUserDetails(username, "******", cachedRole);}// 2. 缓存未命中,查询数据库User user = userMapper.findByUsername(username);if (user == null) {throw new UsernameNotFoundException("❌ 用户不存在: " + username);}// 3. 将角色写入 Redis,有效期 30 分钟redisService.set("user_role:" + username, user.getRole(), 30);System.out.println("🔥 数据库查询并缓存: " + username);return buildUserDetails(user.getUsername(), user.getPassword(), user.getRole());}/*** 构建 Spring Security 的 UserDetails 对象* @param username 用户名* @param password 加密后的密码* @param role 角色(如 ROLE_ADMIN)* @return UserDetails 实例*/private UserDetails buildUserDetails(String username, String password, String role) {// 将角色封装为 GrantedAuthority(权限对象)Collection<? extends GrantedAuthority> authorities =Collections.singletonList(new SimpleGrantedAuthority(role));// 创建 Spring Security 内置用户对象// 参数:用户名、密码、权限集合return new org.springframework.security.core.userdetails.User(username,password,authorities);}
}
6. SecurityConfig.java
- 安全配置
package com.example.demo.config;import com.example.demo.service.CustomUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;/*** Spring Security 配置类* 控制认证、授权、密码编码、会话等行为*/
@Configuration
@EnableWebSecurity // 启用 Web 安全
@EnableGlobalMethodSecurity(prePostEnabled = true) // 启用方法级安全(支持 @PreAuthorize)
public class SecurityConfig {@Autowiredprivate CustomUserDetailsService userDetailsService;/*** 密码编码器 Bean* 用于比对用户输入密码与数据库加密密码* @return BCryptPasswordEncoder 实例*/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}/*** 安全过滤链配置* 定义哪些请求需要认证、使用何种认证方式等*/@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http.csrf().disable() // 禁用 CSRF(适合无状态 API).sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态会话.and().authorizeHttpRequests(authz -> authz.requestMatchers("/login").permitAll() // 登录接口放行.anyRequest().authenticated() // 其他请求需认证).httpBasic(); // 使用 HTTP Basic 认证(测试用)return http.build();}/*** Redis Template Bean* 用于操作 Redis* Spring Boot 自动配置 RedisConnectionFactory*/@Beanpublic StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {return new StringRedisTemplate(factory);}
}
7. UserController.java
- 控制器
package com.example.demo.controller;import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;/*** 用户操作控制器* 演示 @PreAuthorize 方法级权限控制*/
@RestController
@RequestMapping("/api/users")
public class UserController {/*** 删除用户接口* @PreAuthorize("hasRole('ROLE_ADMIN')")* 只有拥有 ROLE_ADMIN 角色的用户才能调用* 注意:hasRole() 会自动添加 ROLE_ 前缀,所以写 'ADMIN' 也可以*/@PreAuthorize("hasRole('ROLE_ADMIN')")@DeleteMapping("/{username}")public String deleteUser(@PathVariable String username) {return "🗑️ 用户 " + username + " 已删除";}/*** 查看用户信息* @PreAuthorize("authentication.principal.username == #username")* 表达式含义:* 当前登录用户名(authentication.principal.username)* 必须等于路径参数 #username* 实现“只能查看自己信息”的业务逻辑*/@PreAuthorize("authentication.principal.username == #username")@GetMapping("/{username}")public String getUserInfo(@PathVariable String username) {return "👤 用户信息: " + username;}
}
8. application.yml
- 配置文件
server:port: 8080 # 服务端口spring:datasource:url: jdbc:mysql://localhost:3306/security_demo?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghaiusername: rootpassword: yourpassword # 请替换为真实密码driver-class-name: com.mysql.cj.jdbc.Driverredis:host: localhostport: 6379 # Redis 服务地址mybatis:type-aliases-package: com.example.demo.entity # 别名包,SQL 中可用类名代替全路径configuration:map-underscore-to-camel-case: true # 数据库下划线字段自动映射到 Java 驼峰属性logging:level:com.example.demo.mapper: debug # 显示 MyBatis 执行的 SQL
▶️ 第四步:运行与测试
1. 启动项目
服务启动后,访问
http://localhost:8080
会跳转登录页(Basic Auth)。
2. 测试命令
✅ 测试1:管理员查看自己
curl -u admin:123456 http://localhost:8080/api/users/admin
# 响应:👤 用户信息: admin
# 说明:用户名匹配,授权通过
✅ 测试2:管理员删除用户
curl -X DELETE -u admin:123456 http://localhost:8080/api/users/alice
# 响应:🗑️ 用户 alice 已删除
# 说明:admin 拥有 ROLE_ADMIN,权限通过
❌ 测试3:普通用户删别人
curl -X DELETE -u alice:123456 http://localhost:8080/api/users/bob
# 响应:403 Forbidden
# 说明:alice 是 ROLE_USER,不满足 hasRole('ROLE_ADMIN')
✅ 查看 Redis 缓存
redis-cli
> KEYS user_role:*
# 输出:
# "user_role:admin"
# "user_role:alice"
# "user_role:bob"
> GET user_role:admin
# "ROLE_ADMIN"
🏁 总结:核心知识点
技术 | 作用 |
---|---|
@PreAuthorize | 方法级权限控制,支持 SpEL 表达式 |
hasRole() | 检查角色(自动加 ROLE_ 前缀) |
authentication.principal.username | 获取当前登录用户名 |
#param | 引用方法参数 |
Redis 缓存 | 提升性能,避免重复查库 |
BCrypt | 安全存储密码 |
HTTP Basic | 简单认证方式(适合测试) |
KEY: user_role:admin
VAL: ROLE_ADMINKEY: user_role:alice
VAL: ROLE_USER