docs: 架构文档补充Shiro认证框架和服务调用链uid/uname传递设计

This commit is contained in:
zhangjf 2026-02-15 10:45:16 +08:00
parent 2088742543
commit 2a45ac0279

View File

@ -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 |
---