feat(tenant): 实现 ProjectSecurityWebFilter 项目权限集合校验

新增 ProjectSecurityWebFilter:
- 集合校验: user.authorizedProjectIds.contains(header.projectId)
- 默认项目选择: DEFAULT编码 → 最小ID → 单项目自动选中 → 无授权403
- @ProjectIgnore URL 自动跳过
- 注册在 WebFilterOrderEnum.PROJECT_SECURITY_FILTER (-98)

框架层:
- ProjectCommonApi: 新增 getAuthorizedProjectIds, getDefaultProjectId
- ProjectFrameworkService: 新增授权查询 + Caffeine 缓存(60s/1000条)
- ViewshTenantAutoConfiguration: 注册 Filter + 扫描 @ProjectIgnore

业务层:
- ProjectService: 新增 getAuthorizedProjectIds, getDefaultProjectId
- ProjectServiceImpl: 默认项目3级回退逻辑
- ProjectApiImpl: 实现 Feign 端点

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
lzh
2026-04-16 23:35:56 +08:00
parent c85f84ea46
commit 423bf3ec3f
8 changed files with 320 additions and 0 deletions

View File

@@ -31,4 +31,14 @@ public class ProjectApiImpl implements ProjectCommonApi {
return success(true);
}
@Override
public CommonResult<List<Long>> getAuthorizedProjectIds(Long userId) {
return success(projectService.getAuthorizedProjectIds(userId));
}
@Override
public CommonResult<Long> getDefaultProjectId(Long userId) {
return success(projectService.getDefaultProjectId(userId));
}
}

View File

@@ -93,4 +93,21 @@ public interface ProjectService {
*/
Long createDefaultProject(Long tenantId, String tenantName);
/**
* 获得用户授权的项目编号列表
*
* @param userId 用户编号
* @return 项目编号列表
*/
List<Long> getAuthorizedProjectIds(Long userId);
/**
* 获得用户的默认项目编号
* 逻辑1.找 DEFAULT 编码 → 2.取最小 ID → 3.无授权返回 null
*
* @param userId 用户编号
* @return 默认项目编号,无授权返回 null
*/
Long getDefaultProjectId(Long userId);
}

View File

@@ -1,5 +1,6 @@
package com.viewsh.module.system.service.project;
import cn.hutool.core.collection.CollUtil;
import com.viewsh.framework.common.enums.CommonStatusEnum;
import com.viewsh.framework.common.pojo.PageResult;
import com.viewsh.framework.common.util.collection.CollectionUtils;
@@ -138,6 +139,33 @@ public class ProjectServiceImpl implements ProjectService {
return project.getId();
}
@Override
public List<Long> getAuthorizedProjectIds(Long userId) {
List<UserProjectDO> userProjects = userProjectMapper.selectListByUserId(userId);
return CollectionUtils.convertList(userProjects, UserProjectDO::getProjectId);
}
@Override
public Long getDefaultProjectId(Long userId) {
List<Long> authorizedProjectIds = getAuthorizedProjectIds(userId);
if (CollUtil.isEmpty(authorizedProjectIds)) {
return null;
}
// 1. 只有一个项目,直接返回
if (authorizedProjectIds.size() == 1) {
return authorizedProjectIds.get(0);
}
// 2. 查找 DEFAULT 项目
for (Long projectId : authorizedProjectIds) {
ProjectDO project = projectMapper.selectById(projectId);
if (project != null && ProjectDO.CODE_DEFAULT.equals(project.getCode())) {
return project.getId();
}
}
// 3. 取最小 ID
return authorizedProjectIds.stream().min(Long::compareTo).orElse(null);
}
private void validateProjectExists(Long id) {
if (projectMapper.selectById(id) == null) {
throw exception(PROJECT_NOT_EXISTS);