From 2a45ac027948d1a8c800e2a6d9dece21a0ca0215 Mon Sep 17 00:00:00 2001 From: zhangjf Date: Sun, 15 Feb 2026 10:45:16 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=9E=B6=E6=9E=84=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E8=A1=A5=E5=85=85Shiro=E8=AE=A4=E8=AF=81=E6=A1=86=E6=9E=B6?= =?UTF-8?q?=E5=92=8C=E6=9C=8D=E5=8A=A1=E8=B0=83=E7=94=A8=E9=93=BEuid/uname?= =?UTF-8?q?=E4=BC=A0=E9=80=92=E8=AE=BE=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/资金服务平台 FundPlatform 架构设计文档.md | 814 +++++++++++++++++- 1 file changed, 793 insertions(+), 21 deletions(-) diff --git a/doc/资金服务平台 FundPlatform 架构设计文档.md b/doc/资金服务平台 FundPlatform 架构设计文档.md index 531c6ee..8ad71dc 100644 --- a/doc/资金服务平台 FundPlatform 架构设计文档.md +++ b/doc/资金服务平台 FundPlatform 架构设计文档.md @@ -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); - - log.debug("[FeignTenantRouting] Add tenant header: {}, group: {}", - tenantId, tenantGroup); + template.header(HEADER_TENANT_GROUP, 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 CURRENT_USER_ID = new ThreadLocal<>(); + private static final ThreadLocal CURRENT_USERNAME = new ThreadLocal<>(); + private static final ThreadLocal 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 + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] [%X{tenantId}] [%X{uid}] [%X{uname}] %-5level %logger{36} - %msg%n + + + +``` + +输出示例: +``` +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 filters = new HashMap<>(); + filters.put("jwt", new JwtAuthenticationFilter()); + filterFactoryBean.setFilters(filters); + + // 过滤器链规则 + Map 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 redisTemplate) { + RedisSessionDAO redisSessionDAO = new RedisSessionDAO(); + redisSessionDAO.setRedisTemplate(redisTemplate); + redisSessionDAO.setKeyPrefix("shiro:session:"); + redisSessionDAO.setExpire(30 * 60); + return redisSessionDAO; + } + + /** + * 缓存管理器(Redis) + */ + @Bean + public CacheManager cacheManager(RedisTemplate 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 roles = permissionService.getUserRoles(userId); + authorizationInfo.setRoles(roles); + + // 查询用户权限 + Set 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 getUserInfo() { + Long userId = UserContext.getCurrentUserId(); + return Result.success(userService.getUserInfo(userId)); + } + + /** + * 需要 admin 角色 + */ + @RequiresRoles("admin") + @GetMapping("/list") + public Result> listUsers(PageParam pageParam) { + return Result.success(userService.listUsers(pageParam)); + } + + /** + * 需要 user:create 权限 + */ + @RequiresPermissions("user:create") + @PostMapping + public Result createUser(@RequestBody @Valid UserCreateDTO dto) { + userService.createUser(dto); + return Result.success(); + } + + /** + * 需要多个权限(逻辑与) + */ + @RequiresPermissions({"user:update", "user:assignRole"}) + @PutMapping("/{userId}/role") + public Result assignRole(@PathVariable Long userId, @RequestBody List 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 | ---