🧱 第一步:环境准备
✅ 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 加密后长度约 60
role 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 {
@Autowired
private UserMapper userMapper;
@Autowired
private RedisService redisService;
/**
* 根据用户名加载用户详情
* 调用时机:用户登录时(/login)
* 流程:
* 1. 先查 Redis 缓存
* 2. 缓存命中 → 返回
* 3. 未命中 → 查数据库 → 写入缓存
* @param username 用户名
* @return UserDetails(Spring Security 用户模型)
* @throws UsernameNotFoundException 用户不存在时抛出
*/
@Override
public 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 {
@Autowired
private CustomUserDetailsService userDetailsService;
/**
* 密码编码器 Bean
* 用于比对用户输入密码与数据库加密密码
* @return BCryptPasswordEncoder 实例
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 安全过滤链配置
* 定义哪些请求需要认证、使用何种认证方式等
*/
@Bean
public 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
*/
@Bean
public 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/Shanghai
username: root
password: yourpassword # 请替换为真实密码
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
host: localhost
port: 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_ADMIN
KEY: user_role:alice
VAL: ROLE_USER