docs: 架构文档补充Shiro认证框架和服务调用链uid/uname传递设计
This commit is contained in:
parent
2088742543
commit
2a45ac0279
@ -728,34 +728,53 @@ public class TenantLoadBalancerConfig {
|
||||
}
|
||||
```
|
||||
|
||||
**4. Feign 租户路由拦截器**
|
||||
**4. Feign 调用链拦截器(租户 + 用户信息传递)**
|
||||
|
||||
```java
|
||||
/**
|
||||
* Feign 租户路由拦截器
|
||||
* 在服务间调用时保持租户上下文
|
||||
* Feign 调用链拦截器
|
||||
* 在服务间调用时传递租户信息和当前操作用户信息
|
||||
*/
|
||||
@Component
|
||||
public class FeignTenantRoutingInterceptor implements RequestInterceptor {
|
||||
public class FeignChainInterceptor implements RequestInterceptor {
|
||||
|
||||
@Autowired
|
||||
private TenantRoutingProperties routingProperties;
|
||||
|
||||
private static final String HEADER_UID = "X-Uid";
|
||||
private static final String HEADER_UNAME = "X-Uname";
|
||||
private static final String HEADER_TENANT_ID = "X-Tenant-Id";
|
||||
private static final String HEADER_TENANT_GROUP = "X-Tenant-Group";
|
||||
|
||||
@Override
|
||||
public void apply(RequestTemplate template) {
|
||||
// 从当前线程上下文获取租户ID
|
||||
// 1. 传递租户信息
|
||||
String tenantId = TenantContextHolder.getTenantId();
|
||||
|
||||
if (StringUtils.isNotEmpty(tenantId)) {
|
||||
// 添加租户ID到请求头
|
||||
template.header(routingProperties.getTenantHeader(), tenantId);
|
||||
template.header(HEADER_TENANT_ID, tenantId);
|
||||
|
||||
// 添加租户组到请求头(用于目标服务的路由)
|
||||
String tenantGroup = buildTenantGroup(tenantId);
|
||||
template.header("X-Tenant-Group", tenantGroup);
|
||||
template.header(HEADER_TENANT_GROUP, tenantGroup);
|
||||
}
|
||||
|
||||
log.debug("[FeignTenantRouting] Add tenant header: {}, group: {}",
|
||||
tenantId, tenantGroup);
|
||||
// 2. 传递当前操作用户信息(关键)
|
||||
Long uid = UserContext.getCurrentUserId();
|
||||
String uname = UserContext.getCurrentUsername();
|
||||
|
||||
if (uid != null) {
|
||||
template.header(HEADER_UID, String.valueOf(uid));
|
||||
log.debug("[FeignChain] Add uid to header: {}", uid);
|
||||
}
|
||||
|
||||
if (StringUtils.isNotEmpty(uname)) {
|
||||
template.header(HEADER_UNAME, uname);
|
||||
log.debug("[FeignChain] Add uname to header: {}", uname);
|
||||
}
|
||||
|
||||
// 3. 传递 TraceId(保持链路追踪)
|
||||
String traceId = TraceIdUtil.getCurrentTraceId();
|
||||
if (StringUtils.isNotEmpty(traceId)) {
|
||||
template.header(TraceIdUtil.TRACE_ID_HEADER, traceId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -765,7 +784,287 @@ public class FeignTenantRoutingInterceptor implements RequestInterceptor {
|
||||
}
|
||||
```
|
||||
|
||||
**5. Nacos 服务注册(带租户标记)**
|
||||
**5. 用户上下文管理(支持调用链传递)**
|
||||
|
||||
```java
|
||||
/**
|
||||
* 用户上下文持有者
|
||||
* 存储当前登录用户信息,支持跨服务传递
|
||||
*/
|
||||
public class UserContext {
|
||||
|
||||
private static final ThreadLocal<Long> CURRENT_USER_ID = new ThreadLocal<>();
|
||||
private static final ThreadLocal<String> CURRENT_USERNAME = new ThreadLocal<>();
|
||||
private static final ThreadLocal<User> CURRENT_USER = new ThreadLocal<>();
|
||||
|
||||
/**
|
||||
* 设置当前用户(登录时调用)
|
||||
*/
|
||||
public static void setCurrentUser(User user) {
|
||||
if (user != null) {
|
||||
CURRENT_USER_ID.set(user.getUserId());
|
||||
CURRENT_USERNAME.set(user.getUsername());
|
||||
CURRENT_USER.set(user);
|
||||
|
||||
// 同时设置到 MDC,用于日志输出
|
||||
MDC.put("uid", String.valueOf(user.getUserId()));
|
||||
MDC.put("uname", user.getUsername());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求头解析用户(服务间调用时)
|
||||
*/
|
||||
public static void setFromHeaders(String uid, String uname) {
|
||||
if (StringUtils.isNotEmpty(uid)) {
|
||||
CURRENT_USER_ID.set(Long.valueOf(uid));
|
||||
MDC.put("uid", uid);
|
||||
}
|
||||
if (StringUtils.isNotEmpty(uname)) {
|
||||
CURRENT_USERNAME.set(uname);
|
||||
MDC.put("uname", uname);
|
||||
}
|
||||
}
|
||||
|
||||
public static Long getCurrentUserId() {
|
||||
return CURRENT_USER_ID.get();
|
||||
}
|
||||
|
||||
public static String getCurrentUsername() {
|
||||
return CURRENT_USERNAME.get();
|
||||
}
|
||||
|
||||
public static User getCurrentUser() {
|
||||
return CURRENT_USER.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前操作用户(用于审计日志)
|
||||
*/
|
||||
public static String getOperator() {
|
||||
String uname = CURRENT_USERNAME.get();
|
||||
return StringUtils.isNotEmpty(uname) ? uname : "system";
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
CURRENT_USER_ID.remove();
|
||||
CURRENT_USERNAME.remove();
|
||||
CURRENT_USER.remove();
|
||||
MDC.remove("uid");
|
||||
MDC.remove("uname");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**6. 调用链上下文过滤器(接收方)**
|
||||
|
||||
```java
|
||||
/**
|
||||
* 调用链上下文过滤器
|
||||
* 接收上游服务传递的租户信息和用户信息
|
||||
*/
|
||||
@Component
|
||||
@Order(-90) // 在租户过滤器之后,业务过滤器之前
|
||||
public class ChainContextFilter implements Filter {
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response,
|
||||
FilterChain chain) throws IOException, ServletException {
|
||||
HttpServletRequest httpRequest = (HttpServletRequest) request;
|
||||
|
||||
try {
|
||||
// 1. 解析租户信息
|
||||
String tenantId = httpRequest.getHeader("X-Tenant-Id");
|
||||
String tenantGroup = httpRequest.getHeader("X-Tenant-Group");
|
||||
|
||||
if (StringUtils.isNotEmpty(tenantId)) {
|
||||
TenantContextHolder.setTenantId(tenantId);
|
||||
TenantContextHolder.setTenantGroup(tenantGroup);
|
||||
MDC.put("tenantId", tenantId);
|
||||
}
|
||||
|
||||
// 2. 解析用户信息(来自上游服务)
|
||||
String uid = httpRequest.getHeader("X-Uid");
|
||||
String uname = httpRequest.getHeader("X-Uname");
|
||||
|
||||
if (StringUtils.isNotEmpty(uid)) {
|
||||
UserContext.setFromHeaders(uid, uname);
|
||||
log.debug("[ChainContext] Received from upstream - uid: {}, uname: {}", uid, uname);
|
||||
} else {
|
||||
// 如果没有上游传递,尝试从 Shiro 获取(网关直接访问)
|
||||
Subject subject = SecurityUtils.getSubject();
|
||||
if (subject.isAuthenticated()) {
|
||||
Long userId = (Long) subject.getPrincipal();
|
||||
User user = userService.getById(userId);
|
||||
if (user != null) {
|
||||
UserContext.setCurrentUser(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 解析 TraceId
|
||||
String traceId = httpRequest.getHeader(TraceIdUtil.TRACE_ID_HEADER);
|
||||
if (StringUtils.isNotEmpty(traceId)) {
|
||||
TraceIdUtil.setTraceId(traceId);
|
||||
}
|
||||
|
||||
chain.doFilter(request, response);
|
||||
} finally {
|
||||
// 清理所有上下文
|
||||
TenantContextHolder.clear();
|
||||
UserContext.clear();
|
||||
TraceIdUtil.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**7. 调用链传递示意图**
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 服务调用链用户信息传递 │
|
||||
│ │
|
||||
│ 用户请求 │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────┐ Headers: │
|
||||
│ │ Gateway │ Authorization: Bearer xxx │
|
||||
│ │ (网关) │ X-Tenant-Id: tenant_001 │
|
||||
│ └──────┬──────┘ │
|
||||
│ │ │
|
||||
│ │ 解析 Token 获取 uid/uname │
|
||||
│ │ 存入 UserContext │
|
||||
│ ▼ │
|
||||
│ ┌─────────────┐ Headers: │
|
||||
│ │ fund-sys │ X-Tenant-Id: tenant_001 │
|
||||
│ │ (用户服务) │ X-Tenant-Group: TENANT_001 │
|
||||
│ │ │ X-Uid: 1001 ◄── 当前操作用户ID │
|
||||
│ │ │ X-Uname: admin ◄── 当前操作用户名 │
|
||||
│ │ │ X-Trace-Id: abc123 │
|
||||
│ └──────┬──────┘ │
|
||||
│ │ │
|
||||
│ │ Feign 调用 fund-cust 服务 │
|
||||
│ │ FeignChainInterceptor 自动添加 Headers │
|
||||
│ ▼ │
|
||||
│ ┌─────────────┐ Headers: │
|
||||
│ │ fund-cust │ X-Tenant-Id: tenant_001 │
|
||||
│ │ (客户服务) │ X-Tenant-Group: TENANT_001 │
|
||||
│ │ │ X-Uid: 1001 ◄── 透传用户ID │
|
||||
│ │ │ X-Uname: admin ◄── 透传用户名 │
|
||||
│ │ │ X-Trace-Id: abc123 │
|
||||
│ └──────┬──────┘ │
|
||||
│ │ │
|
||||
│ │ Feign 调用 fund-proj 服务 │
|
||||
│ ▼ │
|
||||
│ ┌─────────────┐ Headers: │
|
||||
│ │ fund-proj │ X-Tenant-Id: tenant_001 │
|
||||
│ │ (项目服务) │ X-Tenant-Group: TENANT_001 │
|
||||
│ │ │ X-Uid: 1001 ◄── 持续透传 │
|
||||
│ │ │ X-Uname: admin ◄── 持续透传 │
|
||||
│ └─────────────┘ │
|
||||
│ │
|
||||
│ 特点: │
|
||||
│ • 每个服务都能获取原始操作用户(uid/uname) │
|
||||
│ • 用于操作审计、数据权限控制、日志记录 │
|
||||
│ • 无需在每个服务重复解析 Token │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**8. 使用场景示例**
|
||||
|
||||
```java
|
||||
/**
|
||||
* 项目服务 - 创建项目
|
||||
*/
|
||||
@Service
|
||||
public class ProjectServiceImpl implements ProjectService {
|
||||
|
||||
@Autowired
|
||||
private ProjectMapper projectMapper;
|
||||
|
||||
@Autowired
|
||||
private OperationLogService logService;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void createProject(ProjectCreateDTO dto) {
|
||||
// 1. 获取当前操作用户(来自调用链传递)
|
||||
Long operatorId = UserContext.getCurrentUserId();
|
||||
String operatorName = UserContext.getCurrentUsername();
|
||||
|
||||
// 2. 设置项目创建人
|
||||
Project project = new Project();
|
||||
project.setProjectName(dto.getProjectName());
|
||||
project.setCustomerId(dto.getCustomerId());
|
||||
project.setCreatedBy(operatorId); // 使用传递过来的 uid
|
||||
project.setCreatedTime(LocalDateTime.now());
|
||||
|
||||
projectMapper.insert(project);
|
||||
|
||||
// 3. 记录操作日志(包含操作人信息)
|
||||
logService.saveLog(new OperationLog()
|
||||
.setOperation("创建项目")
|
||||
.setOperatorId(operatorId)
|
||||
.setOperatorName(operatorName) // 使用传递过来的 uname
|
||||
.setTargetId(project.getProjectId())
|
||||
.setContent("创建项目:" + dto.getProjectName())
|
||||
);
|
||||
|
||||
log.info("[Project] 用户 {} 创建了项目 {}", operatorName, project.getProjectName());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作日志服务 - 记录日志
|
||||
*/
|
||||
@Service
|
||||
public class OperationLogServiceImpl implements OperationLogService {
|
||||
|
||||
@Autowired
|
||||
private OperationLogMapper logMapper;
|
||||
|
||||
@Override
|
||||
public void saveLog(OperationLog log) {
|
||||
// 自动补充操作人信息(如果未设置)
|
||||
if (log.getOperatorId() == null) {
|
||||
log.setOperatorId(UserContext.getCurrentUserId());
|
||||
}
|
||||
if (StringUtils.isEmpty(log.getOperatorName())) {
|
||||
log.setOperatorName(UserContext.getCurrentUsername());
|
||||
}
|
||||
|
||||
// 补充租户信息
|
||||
log.setTenantId(TenantContextHolder.getTenantId());
|
||||
|
||||
logMapper.insert(log);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**9. 日志格式(包含 uid/uname)**
|
||||
|
||||
```xml
|
||||
<!-- logback-spring.xml -->
|
||||
<configuration>
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<!-- 格式: 时间 [线程] [TraceId] [租户ID] [用户ID] [用户名] 级别 类名 - 消息 -->
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] [%X{tenantId}] [%X{uid}] [%X{uname}] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
</configuration>
|
||||
```
|
||||
|
||||
输出示例:
|
||||
```
|
||||
2026-02-13 14:30:25.123 [http-nio-8120-exec-1] [abc123def456] [tenant_001] [1001] [admin] INFO c.f.p.s.ProjectService - 用户 admin 创建了项目 资金管理系统
|
||||
```
|
||||
|
||||
**原 4. Feign 租户路由拦截器(已合并到 FeignChainInterceptor)**
|
||||
|
||||
|
||||
**10. Nacos 服务注册(带租户标记)**
|
||||
|
||||
```java
|
||||
/**
|
||||
@ -795,7 +1094,7 @@ public class NacosTenantRegistrationConfig {
|
||||
}
|
||||
```
|
||||
|
||||
**6. 租户上下文管理**
|
||||
**11. 租户上下文管理**
|
||||
|
||||
```java
|
||||
/**
|
||||
@ -861,7 +1160,7 @@ public class TenantContextFilter implements Filter {
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.6.3 配置示例
|
||||
#### 2.6.3 配置示例(包含调用链配置)
|
||||
|
||||
```yaml
|
||||
# application-tenant.yml
|
||||
@ -921,7 +1220,40 @@ tenant:
|
||||
password: xxx
|
||||
```
|
||||
|
||||
#### 2.6.4 部署架构
|
||||
#### 2.6.4 调用链配置示例
|
||||
|
||||
```
|
||||
```yaml
|
||||
# application.yml - 调用链配置
|
||||
spring:
|
||||
cloud:
|
||||
openfeign:
|
||||
client:
|
||||
config:
|
||||
default:
|
||||
# Feign 拦截器配置
|
||||
requestInterceptors:
|
||||
- com.fundplatform.common.feign.FeignChainInterceptor
|
||||
# 连接超时
|
||||
connectTimeout: 5000
|
||||
# 读取超时
|
||||
readTimeout: 10000
|
||||
|
||||
# 调用链上下文过滤器配置
|
||||
chain:
|
||||
context:
|
||||
# 是否启用调用链传递
|
||||
enabled: true
|
||||
# 传递的 Headers
|
||||
propagate-headers:
|
||||
- X-Tenant-Id
|
||||
- X-Tenant-Group
|
||||
- X-Uid
|
||||
- X-Uname
|
||||
- X-Trace-Id
|
||||
```
|
||||
|
||||
#### 2.6.5 部署架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
@ -1447,7 +1779,7 @@ public class PerformanceAspect {
|
||||
| **ORM框架** | MyBatis-Plus | 3.5.x | 数据访问层 |
|
||||
| **数据库连接池** | Druid | 1.2.x | 连接池、监控 |
|
||||
| **缓存框架** | Spring Data Redis | 3.x | Redis操作 |
|
||||
| **安全框架** | Spring Security | 6.x | 认证授权 |
|
||||
| **安全框架** | Apache Shiro | 2.x | 认证授权、会话管理 |
|
||||
| **JWT令牌** | jjwt | 0.12.x | Token生成与验证 |
|
||||
| **API文档** | Knife4j | 4.x | Swagger增强 |
|
||||
| **对象存储** | 腾讯云COS SDK | 5.x | 文件存储 |
|
||||
@ -1556,9 +1888,91 @@ public class PerformanceAspect {
|
||||
|
||||
## 五、安全架构
|
||||
|
||||
### 5.1 认证授权
|
||||
### 5.1 认证授权(Apache Shiro)
|
||||
|
||||
#### 5.1.1 JWT认证流程
|
||||
#### 5.1.1 Shiro 架构设计
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Shiro 认证授权架构 │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Subject(主体) │ │
|
||||
│ │ 封装了当前用户的登录状态、权限信息 │ │
|
||||
│ └────────────────────────────────┬────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ SecurityManager(安全管理器) │ │
|
||||
│ │ 核心组件,管理所有 Subject │ │
|
||||
│ └──────────────┬─────────────────┼─────────────────┬───────────────────┘ │
|
||||
│ │ │ │ │
|
||||
│ ▼ ▼ ▼ │
|
||||
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
|
||||
│ │ Authenticator │ │ Authorizer │ │ SessionManager │ │
|
||||
│ │ (认证器) │ │ (授权器) │ │ (会话管理器) │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ • 登录认证 │ │ • 角色校验 │ │ • 会话创建 │ │
|
||||
│ │ • 多Realm认证 │ │ • 权限校验 │ │ • 会话存储 │ │
|
||||
│ │ • rememberMe │ │ • 注解权限 │ │ • 会话超时 │ │
|
||||
│ └────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘ │
|
||||
│ │ │ │ │
|
||||
│ ▼ ▼ ▼ │
|
||||
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
|
||||
│ │ Realm │ │ SessionDAO │ │ CacheManager │ │
|
||||
│ │ (领域) │ │ (会话持久化) │ │ (缓存管理) │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ • 用户数据查询 │ │ • Redis存储 │ │ • 认证缓存 │ │
|
||||
│ │ • 密码比对 │ │ • 分布式会话 │ │ • 授权缓存 │ │
|
||||
│ │ • 权限数据加载 │ │ │ │ • 会话缓存 │ │
|
||||
│ └──────────────────┘ └──────────────────┘ └──────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### 5.1.2 Shiro + JWT 认证流程
|
||||
|
||||
```
|
||||
┌─────────┐ ┌─────────┐
|
||||
│ 客户端 │ │ 服务端 │
|
||||
└────┬────┘ └────┬────┘
|
||||
│ │
|
||||
│ 1. 登录请求 (username/password) │
|
||||
│ ──────────────────────────────────────> │
|
||||
│ │
|
||||
│ ┌─────────┴─────────┐
|
||||
│ │ Shiro Realm 认证 │
|
||||
│ │ • 查询用户数据 │
|
||||
│ │ • 密码比对(Bcrypt)│
|
||||
│ │ • 加载用户权限 │
|
||||
│ └─────────┬─────────┘
|
||||
│ │
|
||||
│ 2. 生成 JWT Token │
|
||||
│ {uid, uname, tenantId, roles, perm} │
|
||||
│ <────────────────────────────────────── │
|
||||
│ │
|
||||
│ 3. 业务请求 (Header: Authorization) │
|
||||
│ Header: X-Uid: 1001 │
|
||||
│ Header: X-Uname: admin │
|
||||
│ ──────────────────────────────────────> │
|
||||
│ │
|
||||
│ ┌─────────┴─────────┐
|
||||
│ │ JWT Filter 验证 │
|
||||
│ │ • Token合法性 │
|
||||
│ │ • Token过期检查 │
|
||||
│ │ • 刷新Token(可选) │
|
||||
│ └─────────┬─────────┘
|
||||
│ │
|
||||
│ ┌─────────┴─────────┐
|
||||
│ │ Shiro 授权检查 │
|
||||
│ │ • @RequiresRoles │
|
||||
│ │ • @RequiresPerms │
|
||||
│ └─────────┬─────────┘
|
||||
│ │
|
||||
│ 4. 返回业务数据 │
|
||||
│ <────────────────────────────────────── │
|
||||
```
|
||||
|
||||
#### 5.1.3 Shiro 核心配置
|
||||
|
||||
```
|
||||
┌─────────┐ ┌─────────┐
|
||||
@ -1588,7 +2002,364 @@ public class PerformanceAspect {
|
||||
│ <────────────────────────────────────── │
|
||||
```
|
||||
|
||||
#### 5.1.2 权限模型
|
||||
#### 5.1.3 Shiro 核心实现
|
||||
|
||||
**1. Shiro 配置类**
|
||||
|
||||
```java
|
||||
/**
|
||||
* Shiro 配置类
|
||||
*/
|
||||
@Configuration
|
||||
public class ShiroConfig {
|
||||
|
||||
/**
|
||||
* SecurityManager 配置
|
||||
*/
|
||||
@Bean
|
||||
public DefaultWebSecurityManager securityManager(
|
||||
JwtRealm jwtRealm,
|
||||
SessionManager sessionManager,
|
||||
CacheManager cacheManager) {
|
||||
|
||||
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
|
||||
|
||||
// 配置 Realm
|
||||
securityManager.setRealm(jwtRealm);
|
||||
|
||||
// 配置会话管理器
|
||||
securityManager.setSessionManager(sessionManager);
|
||||
|
||||
// 配置缓存管理器
|
||||
securityManager.setCacheManager(cacheManager);
|
||||
|
||||
// 配置记住我
|
||||
securityManager.setRememberMeManager(rememberMeManager());
|
||||
|
||||
return securityManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shiro 过滤器链配置
|
||||
*/
|
||||
@Bean
|
||||
public ShiroFilterFactoryBean shiroFilterFactoryBean(
|
||||
DefaultWebSecurityManager securityManager) {
|
||||
|
||||
ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
|
||||
filterFactoryBean.setSecurityManager(securityManager);
|
||||
|
||||
// 自定义过滤器
|
||||
Map<String, Filter> filters = new HashMap<>();
|
||||
filters.put("jwt", new JwtAuthenticationFilter());
|
||||
filterFactoryBean.setFilters(filters);
|
||||
|
||||
// 过滤器链规则
|
||||
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
|
||||
filterChainDefinitionMap.put("/api/auth/login", "anon");
|
||||
filterChainDefinitionMap.put("/api/auth/logout", "anon");
|
||||
filterChainDefinitionMap.put("/swagger-ui/**", "anon");
|
||||
filterChainDefinitionMap.put("/v3/api-docs/**", "anon");
|
||||
filterChainDefinitionMap.put("/**", "jwt");
|
||||
|
||||
filterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
|
||||
|
||||
return filterFactoryBean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话管理器(使用 Redis 实现分布式会话)
|
||||
*/
|
||||
@Bean
|
||||
public SessionManager sessionManager(RedisSessionDAO redisSessionDAO) {
|
||||
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
|
||||
|
||||
// 使用 Redis 存储会话
|
||||
sessionManager.setSessionDAO(redisSessionDAO);
|
||||
|
||||
// 会话超时时间(30分钟)
|
||||
sessionManager.setGlobalSessionTimeout(30 * 60 * 1000);
|
||||
|
||||
// 删除无效会话
|
||||
sessionManager.setDeleteInvalidSessions(true);
|
||||
|
||||
// 会话验证间隔
|
||||
sessionManager.setSessionValidationInterval(10 * 60 * 1000);
|
||||
|
||||
return sessionManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redis 会话 DAO
|
||||
*/
|
||||
@Bean
|
||||
public RedisSessionDAO redisSessionDAO(RedisTemplate<String, Object> redisTemplate) {
|
||||
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
|
||||
redisSessionDAO.setRedisTemplate(redisTemplate);
|
||||
redisSessionDAO.setKeyPrefix("shiro:session:");
|
||||
redisSessionDAO.setExpire(30 * 60);
|
||||
return redisSessionDAO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存管理器(Redis)
|
||||
*/
|
||||
@Bean
|
||||
public CacheManager cacheManager(RedisTemplate<String, Object> redisTemplate) {
|
||||
RedisCacheManager cacheManager = new RedisCacheManager();
|
||||
cacheManager.setRedisTemplate(redisTemplate);
|
||||
cacheManager.setKeyPrefix("shiro:cache:");
|
||||
return cacheManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用 Shiro 注解
|
||||
*/
|
||||
@Bean
|
||||
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
|
||||
DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
|
||||
proxyCreator.setProxyTargetClass(true);
|
||||
return proxyCreator;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(
|
||||
DefaultWebSecurityManager securityManager) {
|
||||
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
|
||||
advisor.setSecurityManager(securityManager);
|
||||
return advisor;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**2. JWT Realm 实现**
|
||||
|
||||
```java
|
||||
/**
|
||||
* JWT Realm - 认证和授权
|
||||
*/
|
||||
@Component
|
||||
public class JwtRealm extends AuthorizingRealm {
|
||||
|
||||
@Autowired
|
||||
private JwtUtil jwtUtil;
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@Autowired
|
||||
private PermissionService permissionService;
|
||||
|
||||
/**
|
||||
* 支持 JWT Token
|
||||
*/
|
||||
@Override
|
||||
public boolean supports(AuthenticationToken token) {
|
||||
return token instanceof JwtToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* 授权(验证权限时调用)
|
||||
*/
|
||||
@Override
|
||||
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
|
||||
// 获取用户ID
|
||||
Long userId = (Long) principals.getPrimaryPrincipal();
|
||||
|
||||
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
|
||||
|
||||
// 查询用户角色
|
||||
Set<String> roles = permissionService.getUserRoles(userId);
|
||||
authorizationInfo.setRoles(roles);
|
||||
|
||||
// 查询用户权限
|
||||
Set<String> permissions = permissionService.getUserPermissions(userId);
|
||||
authorizationInfo.setStringPermissions(permissions);
|
||||
|
||||
return authorizationInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证(登录时调用)
|
||||
*/
|
||||
@Override
|
||||
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
|
||||
throws AuthenticationException {
|
||||
|
||||
JwtToken jwtToken = (JwtToken) token;
|
||||
String jwt = jwtToken.getToken();
|
||||
|
||||
// 验证 JWT
|
||||
if (!jwtUtil.validateToken(jwt)) {
|
||||
throw new AuthenticationException("Token 无效或已过期");
|
||||
}
|
||||
|
||||
// 解析 JWT 获取用户ID
|
||||
Long userId = jwtUtil.getUserId(jwt);
|
||||
String username = jwtUtil.getUsername(jwt);
|
||||
|
||||
// 查询用户信息
|
||||
User user = userService.getById(userId);
|
||||
if (user == null) {
|
||||
throw new UnknownAccountException("用户不存在");
|
||||
}
|
||||
|
||||
// 检查用户状态
|
||||
if (user.getStatus() == 0) {
|
||||
throw new LockedAccountException("账号已被禁用");
|
||||
}
|
||||
|
||||
// 将用户信息存入上下文(供后续使用)
|
||||
UserContext.setCurrentUser(user);
|
||||
|
||||
// 返回认证信息
|
||||
return new SimpleAuthenticationInfo(
|
||||
userId, // 主体(用户ID)
|
||||
jwt, // 凭证(JWT)
|
||||
getName() // Realm 名称
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JWT Token 封装
|
||||
*/
|
||||
public class JwtToken implements AuthenticationToken {
|
||||
|
||||
private final String token;
|
||||
|
||||
public JwtToken(String token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return token;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return token;
|
||||
}
|
||||
|
||||
public String getToken() {
|
||||
return token;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**3. JWT 过滤器**
|
||||
|
||||
```java
|
||||
/**
|
||||
* JWT 认证过滤器
|
||||
*/
|
||||
public class JwtAuthenticationFilter extends BasicHttpAuthenticationFilter {
|
||||
|
||||
@Override
|
||||
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
|
||||
HttpServletRequest httpRequest = (HttpServletRequest) request;
|
||||
String authorization = httpRequest.getHeader("Authorization");
|
||||
return authorization != null && authorization.startsWith("Bearer ");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
|
||||
HttpServletRequest httpRequest = (HttpServletRequest) request;
|
||||
String authorization = httpRequest.getHeader("Authorization");
|
||||
String token = authorization.substring(7);
|
||||
|
||||
JwtToken jwtToken = new JwtToken(token);
|
||||
|
||||
try {
|
||||
// 提交给 Realm 进行认证
|
||||
getSubject(request, response).login(jwtToken);
|
||||
return true;
|
||||
} catch (AuthenticationException e) {
|
||||
// 认证失败
|
||||
HttpServletResponse httpResponse = (HttpServletResponse) response;
|
||||
httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
httpResponse.setContentType("application/json;charset=UTF-8");
|
||||
httpResponse.getWriter().write("{\"code\":401,\"message\":\"认证失败\"}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
|
||||
if (isLoginAttempt(request, response)) {
|
||||
try {
|
||||
return executeLogin(request, response);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
|
||||
HttpServletResponse httpResponse = (HttpServletResponse) response;
|
||||
httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
httpResponse.setContentType("application/json;charset=UTF-8");
|
||||
httpResponse.getWriter().write("{\"code\":401,\"message\":\"请先登录\"}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**4. 权限注解使用**
|
||||
|
||||
```java
|
||||
/**
|
||||
* 用户控制器
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/user")
|
||||
public class UserController {
|
||||
|
||||
/**
|
||||
* 需要登录
|
||||
*/
|
||||
@RequiresAuthentication
|
||||
@GetMapping("/info")
|
||||
public Result<UserVO> getUserInfo() {
|
||||
Long userId = UserContext.getCurrentUserId();
|
||||
return Result.success(userService.getUserInfo(userId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 需要 admin 角色
|
||||
*/
|
||||
@RequiresRoles("admin")
|
||||
@GetMapping("/list")
|
||||
public Result<Page<UserVO>> listUsers(PageParam pageParam) {
|
||||
return Result.success(userService.listUsers(pageParam));
|
||||
}
|
||||
|
||||
/**
|
||||
* 需要 user:create 权限
|
||||
*/
|
||||
@RequiresPermissions("user:create")
|
||||
@PostMapping
|
||||
public Result<Void> createUser(@RequestBody @Valid UserCreateDTO dto) {
|
||||
userService.createUser(dto);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 需要多个权限(逻辑与)
|
||||
*/
|
||||
@RequiresPermissions({"user:update", "user:assignRole"})
|
||||
@PutMapping("/{userId}/role")
|
||||
public Result<Void> assignRole(@PathVariable Long userId, @RequestBody List<Long> roleIds) {
|
||||
userService.assignRoles(userId, roleIds);
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 5.1.4 权限模型
|
||||
|
||||
采用 **RBAC** (Role-Based Access Control) 模型:
|
||||
|
||||
@ -1811,6 +2582,7 @@ fund-sys/
|
||||
|------|----------|----------|--------|
|
||||
| v1.0 | 2026-02-13 | 初始版本 | zhangjf |
|
||||
| v1.1 | 2026-02-13 | 补充多租户架构(一库多租户/一库一租户)和 Head 日志追踪设计 | zhangjf |
|
||||
| v1.2 | 2026-02-13 | 补充 Shiro 认证框架、服务调用链 uid/uname 传递设计 | zhangjf |
|
||||
|
||||
---
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user