feat: 实现完整的 RBAC 权限管理系统与基础设施增强
在初始认证基础上,新增完整的 RBAC 权限模型(角色、权限、菜单三级管理), 集成审计日志、接口限流、登录失败锁定、Refresh Token 机制、Redis 分布式缓存与锁、 RocketMQ 消息队列,并引入 Flyway 数据库版本管理,同时补充项目文档与使用示例
This commit is contained in:
559
README_USAGE.md
Normal file
559
README_USAGE.md
Normal file
@@ -0,0 +1,559 @@
|
||||
# Spring Boot 模板项目使用文档
|
||||
|
||||
## 项目概述
|
||||
|
||||
这是一个功能完整的 Spring Boot 3.x 项目模板,集成了以下核心功能:
|
||||
|
||||
- **用户认证与授权**:基于 JWT + Spring Security
|
||||
- **RBAC 权限模型**:角色-权限-菜单三级权限控制
|
||||
- **数据库迁移**:Flyway 版本化管理
|
||||
- **Redis 缓存**:多种场景化缓存支持
|
||||
- **RocketMQ 消息队列**:异步消息处理
|
||||
- **分布式锁**:基于 Redis 实现
|
||||
- **接口限流**:基于 Redis + AOP
|
||||
- **审计日志**:AOP 自动记录操作日志
|
||||
- **账户安全**:登录失败锁定、密码强度校验
|
||||
|
||||
---
|
||||
|
||||
## 1. 快速开始
|
||||
|
||||
### 1.1 环境要求
|
||||
|
||||
- JDK 17+
|
||||
- MySQL 8.0+
|
||||
- Redis 6.0+
|
||||
- RocketMQ 5.x(可选)
|
||||
|
||||
### 1.2 数据库初始化
|
||||
|
||||
项目使用 Flyway 自动管理数据库迁移,启动时会自动执行以下操作:
|
||||
|
||||
1. 创建用户表
|
||||
2. 创建 RBAC 相关表(角色、权限、菜单)
|
||||
3. 创建审计日志表
|
||||
4. 创建 Refresh Token 表
|
||||
|
||||
### 1.3 配置文件
|
||||
|
||||
修改 `application-dev.yaml`:
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:mysql://localhost:3306/your_database?useSSL=false&serverTimezone=Asia/Shanghai
|
||||
username: your_username
|
||||
password: your_password
|
||||
|
||||
data:
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
password: your_redis_password
|
||||
|
||||
jwt:
|
||||
secret: your-secret-key-at-least-64-characters-long
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 核心功能使用
|
||||
|
||||
### 2.1 用户认证与授权
|
||||
|
||||
#### 注册用户
|
||||
|
||||
```java
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
UserDto userDto = new UserDto();
|
||||
userDto.setUsername("testuser");
|
||||
userDto.setPassword("Test123!"); // 必须包含大小写字母、数字、特殊字符
|
||||
userDto.setEmail("test@example.com");
|
||||
|
||||
RestBean<LoginResponseVo> result = userService.register(userDto);
|
||||
```
|
||||
|
||||
#### 用户登录
|
||||
|
||||
```java
|
||||
UserDto loginDto = new UserDto();
|
||||
loginDto.setUsername("testuser");
|
||||
loginDto.setPassword("Test123!");
|
||||
|
||||
RestBean<LoginResponseVo> result = userService.login(loginDto);
|
||||
// 返回 accessToken 和 refreshToken
|
||||
```
|
||||
|
||||
#### 使用 Token 访问接口
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer {accessToken}" \
|
||||
http://localhost:8080/api/v1/user/info
|
||||
```
|
||||
|
||||
#### Token 刷新
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"refreshToken": "{refreshToken}"}' \
|
||||
http://localhost:8080/api/v1/user/refresh
|
||||
```
|
||||
|
||||
#### 登出
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer {accessToken}" \
|
||||
http://localhost:8080/api/v1/user/logout
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.2 RBAC 权限控制
|
||||
|
||||
#### 创建角色
|
||||
|
||||
```java
|
||||
@Autowired
|
||||
private SysRoleService roleService;
|
||||
|
||||
RoleDto roleDto = new RoleDto();
|
||||
roleDto.setRoleCode("ROLE_EDITOR");
|
||||
roleDto.setRoleName("编辑");
|
||||
roleDto.setDescription("内容编辑角色");
|
||||
roleDto.setSortOrder(3);
|
||||
roleDto.setStatus(1);
|
||||
|
||||
RoleVo role = roleService.create(roleDto);
|
||||
```
|
||||
|
||||
#### 为角色分配权限
|
||||
|
||||
```java
|
||||
List<Long> permissionIds = Arrays.asList(1L, 2L, 3L); // 文章创建、编辑、删除
|
||||
roleService.assignPermissions(roleId, permissionIds);
|
||||
```
|
||||
|
||||
#### 为用户分配角色
|
||||
|
||||
```java
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
UserRoleUpdateDto dto = new UserRoleUpdateDto();
|
||||
dto.setRoleIds(Set.of(1L, 2L)); // 角色ID列表
|
||||
|
||||
userService.updateUserRole(userId, dto);
|
||||
```
|
||||
|
||||
#### 在 Controller 中使用权限注解
|
||||
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/articles")
|
||||
public class ArticleController {
|
||||
|
||||
// 只有具有 'article:create' 权限的用户可以访问
|
||||
@PreAuthorize("hasAuthority('article:create')")
|
||||
@PostMapping
|
||||
public RestBean<Void> create(@RequestBody ArticleDto dto) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// 需要管理员角色或文章管理权限
|
||||
@PreAuthorize("hasAnyRole('ROLE_ADMIN') or hasAuthority('article:delete')")
|
||||
@DeleteMapping("/{id}")
|
||||
public RestBean<Void> delete(@PathVariable Long id) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.3 Redis 缓存使用
|
||||
|
||||
#### 基本缓存操作
|
||||
|
||||
```java
|
||||
@Autowired
|
||||
private RedisCache redisCache;
|
||||
|
||||
// 设置缓存(30分钟过期)
|
||||
redisCache.set("user:" + userId, userObject, 30, TimeUnit.MINUTES);
|
||||
|
||||
// 获取缓存
|
||||
User user = redisCache.get("user:" + userId, User.class);
|
||||
|
||||
// 删除缓存
|
||||
redisCache.delete("user:" + userId);
|
||||
```
|
||||
|
||||
#### 计数器(阅读数、点赞数)
|
||||
|
||||
```java
|
||||
// 原子自增
|
||||
long viewCount = redisCache.increment("article:view:" + articleId, 1);
|
||||
|
||||
// 原子自减
|
||||
long stock = redisCache.decrement("product:stock:" + productId, quantity);
|
||||
```
|
||||
|
||||
#### 哈希表(对象字段缓存)
|
||||
|
||||
```java
|
||||
// 设置单个字段
|
||||
redisCache.hSet("user:profile:" + userId, "nickname", "张三");
|
||||
redisCache.hSet("user:profile:" + userId, "age", 25);
|
||||
|
||||
// 获取单个字段
|
||||
String nickname = (String) redisCache.hGet("user:profile:" + userId, "nickname");
|
||||
|
||||
// 获取所有字段
|
||||
Map<Object, Object> profile = redisCache.hGetAll("user:profile:" + userId);
|
||||
```
|
||||
|
||||
#### 列表(消息队列)
|
||||
|
||||
```java
|
||||
// 从右侧推入(队列尾部)
|
||||
redisCache.lRightPush("queue:email", emailObject);
|
||||
|
||||
// 从左侧弹出(队列头部)
|
||||
Object email = redisCache.lLeftPop("queue:email");
|
||||
```
|
||||
|
||||
#### 集合(去重、标签)
|
||||
|
||||
```java
|
||||
// 添加标签(自动去重)
|
||||
redisCache.sAdd("article:tags:" + articleId, "Java", "Spring", "Redis");
|
||||
|
||||
// 检查标签是否存在
|
||||
boolean hasTag = redisCache.sIsMember("article:tags:" + articleId, "Java");
|
||||
|
||||
// 获取所有标签
|
||||
Set<Object> tags = redisCache.sMembers("article:tags:" + articleId);
|
||||
```
|
||||
|
||||
#### 有序集合(排行榜)
|
||||
|
||||
```java
|
||||
// 添加到排行榜
|
||||
redisCache.zAdd("leaderboard:user:score", userId, score);
|
||||
|
||||
// 获取用户排名
|
||||
Long rank = redisCache.zReverseRank("leaderboard:user:score", userId);
|
||||
|
||||
// 获取用户分数
|
||||
Double score = redisCache.zScore("leaderboard:user:score", userId);
|
||||
|
||||
// 增加分数
|
||||
redisCache.zIncrementScore("leaderboard:user:score", userId, 10.0);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.4 分布式锁使用
|
||||
|
||||
```java
|
||||
@Autowired
|
||||
private RedisLock redisLock;
|
||||
|
||||
public void processOrder(Long orderId) {
|
||||
// 1. 获取锁(30秒过期)
|
||||
String lockValue = redisLock.tryLock("order:" + orderId, 30);
|
||||
|
||||
if (lockValue == null) {
|
||||
throw new RuntimeException("订单正在处理中");
|
||||
}
|
||||
|
||||
try {
|
||||
// 2. 执行业务逻辑
|
||||
// ...
|
||||
|
||||
} finally {
|
||||
// 3. 释放锁(只有持有者才能释放)
|
||||
redisLock.unlock("order:" + orderId, lockValue);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.5 接口限流使用
|
||||
|
||||
```java
|
||||
@RestController
|
||||
public class LoginController {
|
||||
|
||||
// 限制每 IP 每分钟最多 5 次登录尝试
|
||||
@RateLimit(permits = 5, seconds = 60, limitType = RateLimit.LimitType.IP)
|
||||
@PostMapping("/login")
|
||||
public RestBean<LoginResponseVo> login(@RequestBody UserDto dto) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.6 审计日志使用
|
||||
|
||||
```java
|
||||
@RestController
|
||||
public class UserController {
|
||||
|
||||
// 自动记录审计日志
|
||||
@AuditLog(action = "update", resource = "user", description = "更新用户信息")
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('user:update')")
|
||||
public RestBean<UserVo> updateUser(@PathVariable Long id, @RequestBody UserDto dto) {
|
||||
// 操作会被自动记录到 sys_audit_log 表
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.7 事务使用
|
||||
|
||||
#### 基本事务
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class UserService {
|
||||
|
||||
// 所有异常都回滚
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void createUserWithRole(UserDto userDto, Set<Long> roleIds) {
|
||||
// 创建用户
|
||||
User user = new User();
|
||||
// ...
|
||||
userRepository.save(user);
|
||||
|
||||
// 分配角色
|
||||
for (Long roleId : roleIds) {
|
||||
SysRole role = roleRepository.findById(roleId)
|
||||
.orElseThrow(() -> new BusinessException("角色不存在"));
|
||||
user.getRoles().add(role);
|
||||
}
|
||||
userRepository.save(user);
|
||||
// 任何异常都会回滚整个事务
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 嵌套事务
|
||||
|
||||
```java
|
||||
// 外层事务
|
||||
@Transactional(propagation = Propagation.REQUIRED)
|
||||
public void outerMethod() {
|
||||
// 业务逻辑
|
||||
innerMethod(); // 加入外层事务
|
||||
}
|
||||
|
||||
// 内层事务
|
||||
@Transactional(propagation = Propagation.REQUIRED)
|
||||
public void innerMethod() {
|
||||
// 加入外层事务
|
||||
}
|
||||
```
|
||||
|
||||
#### 独立事务
|
||||
|
||||
```java
|
||||
@Transactional(propagation = Propagation.REQUIRED)
|
||||
public void mainMethod() {
|
||||
// 主事务逻辑
|
||||
|
||||
// 独立事务(即使主事务回滚也不影响)
|
||||
recordLog();
|
||||
}
|
||||
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
public void recordLog() {
|
||||
// 独立事务
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.8 RocketMQ 消息队列使用
|
||||
|
||||
#### 发送消息
|
||||
|
||||
```java
|
||||
@Autowired
|
||||
private UserMessageProducer messageProducer;
|
||||
|
||||
// 发送注册消息
|
||||
messageProducer.sendRegisterMessage(userId, username, email);
|
||||
|
||||
// 发送登录消息
|
||||
messageProducer.sendLoginMessage(userId, username, ipAddress);
|
||||
```
|
||||
|
||||
#### 消费消息
|
||||
|
||||
消息会被 `UserMessageConsumer` 自动消费,根据消息类型执行不同操作:
|
||||
|
||||
```java
|
||||
@RocketMQMessageListener(
|
||||
consumerGroup = "user-consumer-group",
|
||||
topic = "user-topic"
|
||||
)
|
||||
public class UserMessageConsumer implements RocketMQListener<UserMessage> {
|
||||
|
||||
@Override
|
||||
public void onMessage(UserMessage message) {
|
||||
switch (message.getMessageType()) {
|
||||
case "REGISTER":
|
||||
// 处理注册消息(发送欢迎邮件等)
|
||||
break;
|
||||
case "LOGIN":
|
||||
// 处理登录消息(记录登录日志)
|
||||
break;
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 配置说明
|
||||
|
||||
### 3.1 JWT 配置
|
||||
|
||||
```yaml
|
||||
jwt:
|
||||
# JWT 密钥(至少64字符)
|
||||
secret: your-secret-key-at-least-64-characters-long
|
||||
# Access Token 过期时间(秒)
|
||||
access-token-expiration: 3600 # 1小时
|
||||
# Refresh Token 过期时间(秒)
|
||||
refresh-token-expiration: 604800 # 7天
|
||||
```
|
||||
|
||||
### 3.2 登录安全配置
|
||||
|
||||
```yaml
|
||||
app:
|
||||
login:
|
||||
# 最大失败次数
|
||||
max-attempts: 5
|
||||
# 锁定时长(分钟)
|
||||
lock-duration-minutes: 30
|
||||
```
|
||||
|
||||
### 3.3 Redis 配置
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
data:
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
password: your-password
|
||||
database: 0
|
||||
timeout: 5000ms
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 8
|
||||
max-idle: 8
|
||||
min-idle: 2
|
||||
```
|
||||
|
||||
### 3.4 RocketMQ 配置
|
||||
|
||||
```yaml
|
||||
rocketmq:
|
||||
name-server: localhost:9876
|
||||
producer:
|
||||
group: user-producer-group
|
||||
user-topic: user-topic
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 常见问题
|
||||
|
||||
### 4.1 如何添加新的权限?
|
||||
|
||||
1. 在数据库中插入权限记录(通过 Flyway 迁移脚本)
|
||||
2. 在 Controller 上使用 `@PreAuthorize` 注解
|
||||
|
||||
```java
|
||||
@PreAuthorize("hasAuthority('new:permission')")
|
||||
public void someMethod() { }
|
||||
```
|
||||
|
||||
### 4.2 如何自定义限流规则?
|
||||
|
||||
使用 `@RateLimit` 注解:
|
||||
|
||||
```java
|
||||
@RateLimit(
|
||||
permits = 10, // 10次
|
||||
seconds = 60, // 每分钟
|
||||
limitType = RateLimit.LimitType.USER, // 按用户限流
|
||||
keyPrefix = "custom:" // 自定义键前缀
|
||||
)
|
||||
public void customMethod() { }
|
||||
```
|
||||
|
||||
### 4.3 如何实现缓存预热?
|
||||
|
||||
在应用启动时加载热点数据:
|
||||
|
||||
```java
|
||||
@Component
|
||||
public class CacheWarmupRunner implements ApplicationRunner {
|
||||
|
||||
@Autowired
|
||||
private RedisCache redisCache;
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) {
|
||||
// 预热热点数据
|
||||
List<User> hotUsers = userRepository.findHotUsers();
|
||||
for (User user : hotUsers) {
|
||||
redisCache.set("user:" + user.getId(), user, 1, TimeUnit.HOURS);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 最佳实践
|
||||
|
||||
### 5.1 密码安全
|
||||
|
||||
- 生产环境必须修改 JWT 密钥
|
||||
- 使用强密码策略(已集成 `@StrongPassword`)
|
||||
- 定期更换密码
|
||||
|
||||
### 5.2 数据库事务
|
||||
|
||||
- 查询操作使用 `@Transactional(readOnly = true)`
|
||||
- 明确指定 `rollbackFor = Exception.class`
|
||||
- 避免大事务
|
||||
|
||||
### 5.3 Redis 使用
|
||||
|
||||
- 合理设置过期时间,避免内存溢出
|
||||
- 使用 Redis 分布式锁防止并发问题
|
||||
- 热点数据使用缓存,减少数据库压力
|
||||
|
||||
### 5.4 安全建议
|
||||
|
||||
- 修改 CORS 配置,限制允许的域名
|
||||
- 使用 HTTPS
|
||||
- 定期审查用户权限
|
||||
- 启用审计日志
|
||||
Reference in New Issue
Block a user