feat: 完成工作流 Public API 授权闭环

- 新增访问令牌工作流 API 全局授权与 Public Workflow API 权限断言

- 补齐 API Key 执行记录归属、状态查询与下线后不可恢复边界

- 增加管理端接口调用说明与访问令牌授权开关
This commit is contained in:
2026-05-14 20:41:34 +08:00
parent 47c2bad839
commit da58077d59
15 changed files with 919 additions and 62 deletions

View File

@@ -82,7 +82,7 @@ public class ChainEventListenerForSave implements ChainEventListener {
record.setWorkflowJson(workflow.getContent());
record.setStartTime(new Date());
record.setStatus(state.getStatus().getValue());
record.setCreatedKey(WorkFlowUtil.USER_KEY);
record.setCreatedKey(WorkFlowUtil.getCreatedKey(chain));
record.setCreatedBy(WorkFlowUtil.getOperator(chain).getId().toString());
try {
workflowExecResultService.save(record);

View File

@@ -0,0 +1,38 @@
package tech.easyflow.ai.service;
import tech.easyflow.system.entity.SysApiKey;
import java.math.BigInteger;
/**
* 工作流 Public API 访问令牌授权服务。
*/
public interface WorkflowApiPermissionService {
/**
* 工作流资源类型。
*/
String RESOURCE_TYPE_WORKFLOW = "WORKFLOW";
/**
* 工作流 Public API 调用动作范围。
*/
String ACTION_SCOPE_INVOKE = "INVOKE";
/**
* 按访问令牌维度开启或关闭工作流 Public API 调用授权。
*
* @param apiKeyId 系统访问令牌 ID
* @param enabled 是否启用工作流 API 调用授权
*/
void replaceWorkflowApiEnabled(BigInteger apiKeyId, boolean enabled);
/**
* 断言当前令牌具备工作流 Public API 调用权限。
*
* @param apiKey 原始访问令牌
* @param requestUri 请求地址
* @return 已校验通过的访问令牌实体
*/
SysApiKey assertWorkflowApi(String apiKey, String requestUri);
}

View File

@@ -0,0 +1,130 @@
package tech.easyflow.ai.service.impl;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tech.easyflow.ai.service.WorkflowApiPermissionService;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.system.entity.SysApiKey;
import tech.easyflow.system.entity.SysApiKeyResource;
import tech.easyflow.system.entity.SysApiKeyResourceMapping;
import tech.easyflow.system.service.SysApiKeyResourceMappingService;
import tech.easyflow.system.service.SysApiKeyResourceService;
import tech.easyflow.system.service.SysApiKeyService;
import javax.annotation.Resource;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
/**
* 工作流 Public API 访问令牌授权服务实现。
*/
@Service
public class WorkflowApiPermissionServiceImpl implements WorkflowApiPermissionService {
private static final String RESOURCE_TITLE = "工作流 API 调用";
private static final List<String> WORKFLOW_API_URIS = List.of(
"/public-api/workflow/getByIdOrAlias",
"/public-api/workflow/getRunningParameters",
"/public-api/workflow/runAsync",
"/public-api/workflow/getChainStatus",
"/public-api/workflow/resume"
);
@Resource
private SysApiKeyService sysApiKeyService;
@Resource
private SysApiKeyResourceService resourceService;
@Resource
private SysApiKeyResourceMappingService mappingService;
/**
* {@inheritDoc}
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void replaceWorkflowApiEnabled(BigInteger apiKeyId, boolean enabled) {
if (apiKeyId == null) {
throw new BusinessException("系统访问令牌不能为空");
}
SysApiKey apiKey = sysApiKeyService.getById(apiKeyId);
if (apiKey == null) {
throw new BusinessException("系统访问令牌不存在");
}
mappingService.removeScopedMappings(apiKeyId, RESOURCE_TYPE_WORKFLOW);
if (!enabled) {
return;
}
List<SysApiKeyResourceMapping> rows = new ArrayList<>(WORKFLOW_API_URIS.size());
for (String uri : WORKFLOW_API_URIS) {
SysApiKeyResource resource = ensureResource(uri);
SysApiKeyResourceMapping row = new SysApiKeyResourceMapping();
row.setApiKeyId(apiKeyId);
row.setApiKeyResourceId(resource.getId());
row.setResourceType(RESOURCE_TYPE_WORKFLOW);
row.setActionScope(ACTION_SCOPE_INVOKE);
rows.add(row);
}
if (!rows.isEmpty()) {
mappingService.saveBatch(rows);
}
}
/**
* {@inheritDoc}
*/
@Override
public SysApiKey assertWorkflowApi(String apiKey, String requestUri) {
SysApiKey sysApiKey = sysApiKeyService.getSysApiKey(apiKey);
SysApiKeyResource resource = getResource(requestUri);
QueryWrapper wrapper = QueryWrapper.create()
.eq(SysApiKeyResourceMapping::getApiKeyId, sysApiKey.getId())
.eq(SysApiKeyResourceMapping::getApiKeyResourceId, resource.getId())
.eq(SysApiKeyResourceMapping::getResourceType, RESOURCE_TYPE_WORKFLOW)
.isNull(SysApiKeyResourceMapping::getResourceTargetId)
.eq(SysApiKeyResourceMapping::getActionScope, ACTION_SCOPE_INVOKE);
if (mappingService.count(wrapper) == 0) {
throw new BusinessException("该apiKey无权限调用工作流 API");
}
return sysApiKey;
}
/**
* 获取工作流 Public API 资源。
*
* @param requestInterface 请求地址
* @return API 资源
*/
private SysApiKeyResource getResource(String requestInterface) {
QueryWrapper wrapper = QueryWrapper.create()
.eq(SysApiKeyResource::getRequestInterface, requestInterface);
SysApiKeyResource resource = resourceService.getOne(wrapper);
if (resource == null) {
throw new BusinessException("该接口不存在");
}
return resource;
}
/**
* 确保工作流 Public API 资源已存在。
*
* @param requestInterface 请求地址
* @return API 资源
*/
private SysApiKeyResource ensureResource(String requestInterface) {
QueryWrapper wrapper = QueryWrapper.create()
.eq(SysApiKeyResource::getRequestInterface, requestInterface);
SysApiKeyResource resource = resourceService.getOne(wrapper);
if (resource != null) {
return resource;
}
resource = new SysApiKeyResource();
resource.setRequestInterface(requestInterface);
resource.setTitle(RESOURCE_TITLE);
resourceService.save(resource);
return resource;
}
}

View File

@@ -12,7 +12,9 @@ import java.math.BigInteger;
public class WorkFlowUtil {
public final static String USER_KEY = "user";
public final static String API_KEY = "API_KEY";
public final static String WORKFLOW_KEY = "workflow";
public final static String CREATED_KEY_MEMORY_KEY = "workflowCreatedKey";
public static String removeSensitiveInfo(String originJson) {
JSONObject workflowInfo = JSON.parseObject(originJson);
@@ -35,6 +37,17 @@ public class WorkFlowUtil {
return cache == null ? defaultAccount() : (LoginAccount) cache;
}
/**
* 获取工作流执行记录的执行人标识。
*
* @param chain 当前工作流执行链
* @return 执行人标识
*/
public static String getCreatedKey(Chain chain) {
Object value = chain.getState().getMemory().get(CREATED_KEY_MEMORY_KEY);
return value == null ? USER_KEY : String.valueOf(value);
}
public static LoginAccount defaultAccount() {
LoginAccount account = new LoginAccount();
account.setId(new BigInteger("0"));

View File

@@ -26,6 +26,9 @@ public class SysApiKey extends SysApiKeyBase {
@Column(ignore = true)
private Boolean knowledgeShareEnabled;
@Column(ignore = true)
private Boolean workflowApiEnabled;
@RelationOneToMany(selfField = "id", targetField = "apiKeyId", targetTable = "tb_sys_api_key_resource_mapping")
private List<SysApiKeyResourceMapping> resourcePermissions;
@@ -52,4 +55,12 @@ public class SysApiKey extends SysApiKeyBase {
public void setKnowledgeShareEnabled(Boolean knowledgeShareEnabled) {
this.knowledgeShareEnabled = knowledgeShareEnabled;
}
public Boolean getWorkflowApiEnabled() {
return workflowApiEnabled;
}
public void setWorkflowApiEnabled(Boolean workflowApiEnabled) {
this.workflowApiEnabled = workflowApiEnabled;
}
}