feat: 收口知识库分享链路

- 新增 shareKey 单参数 URL 分享页与失效页

- 新增知识库分享后端鉴权、审计与迁移脚本

- 在访问令牌中增加知识库分享授权入口
This commit is contained in:
2026-04-13 14:44:31 +08:00
parent 8cfe5400fe
commit 31a755a8bc
57 changed files with 5158 additions and 143 deletions

View File

@@ -4,6 +4,8 @@ import com.mybatisflex.core.service.IService;
import tech.easyflow.system.entity.SysApiKey;
import tech.easyflow.system.entity.SysApiKeyResourceMapping;
import java.math.BigInteger;
/**
* apikey-请求接口表 服务层。
*
@@ -13,4 +15,21 @@ import tech.easyflow.system.entity.SysApiKeyResourceMapping;
public interface SysApiKeyResourceMappingService extends IService<SysApiKeyResourceMapping> {
void authInterface(SysApiKey entity);
/**
* 移除指定 apiKey 下某个资源目标的 scope 授权。
*
* @param apiKeyId apiKey ID
* @param resourceType 资源类型
* @param resourceTargetId 资源目标 ID
*/
void removeScopedMappings(BigInteger apiKeyId, String resourceType, BigInteger resourceTargetId);
/**
* 按资源类型移除指定 apiKey 下的全部资源级授权。
*
* @param apiKeyId apiKey ID
* @param resourceType 资源类型
*/
void removeScopedMappings(BigInteger apiKeyId, String resourceType);
}

View File

@@ -3,6 +3,8 @@ package tech.easyflow.system.service;
import com.mybatisflex.core.service.IService;
import tech.easyflow.system.entity.SysApiKey;
import java.math.BigInteger;
/**
* 服务层。
*
@@ -15,4 +17,15 @@ public interface SysApiKeyService extends IService<SysApiKey> {
SysApiKey getSysApiKey(String apiKey);
/**
* 校验资源级作用域权限。
*
* @param apiKeyId apiKey 记录 ID
* @param requestURI 请求 URI
* @param resourceType 资源类型
* @param resourceTargetId 资源目标 ID
* @param actionScope 动作范围
*/
void checkResourceScope(BigInteger apiKeyId, String requestURI, String resourceType, BigInteger resourceTargetId, String actionScope);
}

View File

@@ -46,7 +46,10 @@ public class SysApiKeyResourceMappingServiceImpl extends ServiceImpl<SysApiKeyRe
return;
}
redisLockExecutor.executeWithLock(API_KEY_MAPPING_LOCK_PREFIX + apiKeyId, LOCK_WAIT_TIMEOUT, LOCK_LEASE_TIMEOUT, () -> {
this.remove(QueryWrapper.create().eq(SysApiKeyResourceMapping::getApiKeyId, apiKeyId));
QueryWrapper removeWrapper = QueryWrapper.create()
.eq(SysApiKeyResourceMapping::getApiKeyId, apiKeyId)
.isNull(SysApiKeyResourceMapping::getResourceType);
this.remove(removeWrapper);
if (entity.getPermissionIds() == null || entity.getPermissionIds().isEmpty()) {
return;
}
@@ -66,4 +69,29 @@ public class SysApiKeyResourceMappingServiceImpl extends ServiceImpl<SysApiKeyRe
}
});
}
@Override
@Transactional(rollbackFor = Exception.class)
public void removeScopedMappings(BigInteger apiKeyId, String resourceType, BigInteger resourceTargetId) {
if (apiKeyId == null || resourceTargetId == null || resourceType == null || resourceType.isBlank()) {
return;
}
QueryWrapper wrapper = QueryWrapper.create()
.eq(SysApiKeyResourceMapping::getApiKeyId, apiKeyId)
.eq(SysApiKeyResourceMapping::getResourceType, resourceType)
.eq(SysApiKeyResourceMapping::getResourceTargetId, resourceTargetId);
remove(wrapper);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void removeScopedMappings(BigInteger apiKeyId, String resourceType) {
if (apiKeyId == null || resourceType == null || resourceType.isBlank()) {
return;
}
QueryWrapper wrapper = QueryWrapper.create()
.eq(SysApiKeyResourceMapping::getApiKeyId, apiKeyId)
.eq(SysApiKeyResourceMapping::getResourceType, resourceType);
remove(wrapper);
}
}

View File

@@ -79,4 +79,36 @@ public class SysApiKeyServiceImpl extends ServiceImpl<SysApiKeyMapper, SysApiKey
return one;
}
@Override
public void checkResourceScope(BigInteger apiKeyId, String requestURI, String resourceType, BigInteger resourceTargetId, String actionScope) {
List<String> candidateRequestUris = getCandidateRequestUris(requestURI);
QueryWrapper resourceWrapper = QueryWrapper.create();
resourceWrapper.in(SysApiKeyResource::getRequestInterface, candidateRequestUris);
List<SysApiKeyResource> resources = resourceService.list(resourceWrapper);
if (resources == null || resources.isEmpty()) {
throw new BusinessException("该接口不存在");
}
List<BigInteger> resourceIds = resources.stream()
.map(SysApiKeyResource::getId)
.toList();
QueryWrapper exactScopeWrapper = QueryWrapper.create()
.eq(SysApiKeyResourceMapping::getApiKeyId, apiKeyId)
.in(SysApiKeyResourceMapping::getApiKeyResourceId, resourceIds)
.eq(SysApiKeyResourceMapping::getResourceType, resourceType)
.eq(SysApiKeyResourceMapping::getResourceTargetId, resourceTargetId)
.eq(SysApiKeyResourceMapping::getActionScope, actionScope);
if (mappingService.count(exactScopeWrapper) > 0) {
return;
}
QueryWrapper globalScopeWrapper = QueryWrapper.create();
globalScopeWrapper.eq(SysApiKeyResourceMapping::getApiKeyId, apiKeyId);
globalScopeWrapper.in(SysApiKeyResourceMapping::getApiKeyResourceId, resourceIds);
globalScopeWrapper.eq(SysApiKeyResourceMapping::getResourceType, resourceType);
globalScopeWrapper.isNull(SysApiKeyResourceMapping::getResourceTargetId);
globalScopeWrapper.eq(SysApiKeyResourceMapping::getActionScope, actionScope);
if (mappingService.count(globalScopeWrapper) == 0) {
throw new BusinessException("该apiKey无权限访问当前资源");
}
}
}