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

@@ -23,6 +23,9 @@ public class SysApiKey extends SysApiKeyBase {
@Column(ignore = true)
List<BigInteger> permissionIds;
@Column(ignore = true)
private Boolean knowledgeShareEnabled;
@RelationOneToMany(selfField = "id", targetField = "apiKeyId", targetTable = "tb_sys_api_key_resource_mapping")
private List<SysApiKeyResourceMapping> resourcePermissions;
@@ -41,4 +44,12 @@ public class SysApiKey extends SysApiKeyBase {
public void setPermissionIds(List<BigInteger> permissionIds) {
this.permissionIds = permissionIds;
}
public Boolean getKnowledgeShareEnabled() {
return knowledgeShareEnabled;
}
public void setKnowledgeShareEnabled(Boolean knowledgeShareEnabled) {
this.knowledgeShareEnabled = knowledgeShareEnabled;
}
}

View File

@@ -29,6 +29,24 @@ public class SysApiKeyResourceMappingBase implements Serializable {
@Column(comment = "请求接口资源访问id")
private BigInteger apiKeyResourceId;
/**
* 资源类型
*/
@Column(comment = "资源类型")
private String resourceType;
/**
* 资源目标ID
*/
@Column(comment = "资源目标ID")
private BigInteger resourceTargetId;
/**
* 动作范围
*/
@Column(comment = "动作范围")
private String actionScope;
public BigInteger getId() {
return id;
}
@@ -53,4 +71,28 @@ public class SysApiKeyResourceMappingBase implements Serializable {
this.apiKeyResourceId = apiKeyResourceId;
}
public String getResourceType() {
return resourceType;
}
public void setResourceType(String resourceType) {
this.resourceType = resourceType;
}
public BigInteger getResourceTargetId() {
return resourceTargetId;
}
public void setResourceTargetId(BigInteger resourceTargetId) {
this.resourceTargetId = resourceTargetId;
}
public String getActionScope() {
return actionScope;
}
public void setActionScope(String actionScope) {
this.actionScope = actionScope;
}
}

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无权限访问当前资源");
}
}
}