feat(handoff): P1 后端 knowledge 兑现侧接入 Market token 核验/核销

P1(首条端到端 knowledge):knowledge-server 依赖 market-api,createKnowledgeBindingPrecheck
的 market_kb 来源在 precheck 阶段(token 明文仅此阶段持有、createBinding 的 BindReqVO 无 token)
调 MarketHandoffTokenApi.verify 服务端核验(属主/过期/一次性/owner-action),通过则 consume 核销
(消费换 session),替代此前"只存 hash 不验真"假安全。沿用 precheck @Transactional 事务保
verify/consume/落库原子。

注:执行版原写"createBinding 调 consume",自审修正为 precheck 阶段(token 明文仅此可得)。

验证:knowledge-server 编译(-am 连带刷新 meta-api 等)+ BcBoundaryArchTest 绿(knowledge→
market-api 合规、边界放行 -api)。端到端真实行为(拒伪造/过期 token + 核销 + DB)随 P1 前端 +
handoff-knowledge.spec 真后端 e2e 覆盖。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lili 2026-06-22 09:09:26 -07:00
parent a3b45ae6b5
commit e5d2b52f20
2 changed files with 34 additions and 0 deletions

View File

@ -33,6 +33,12 @@
<artifactId>muse-module-events-api</artifactId>
<version>${revision}</version>
</dependency>
<!-- handoff 兑现:核验/核销 Market 一次性 token(MarketHandoffTokenApi,跨空间安全交接 P1) -->
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>muse-module-market-api</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>muse-module-infra-api</artifactId>

View File

@ -5,6 +5,10 @@ import cn.iocoder.muse.framework.common.util.json.JsonUtils;
import cn.iocoder.muse.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.muse.module.knowledge.application.muse.facade.MuseKnowledgeWorkOwnerFacade;
import cn.iocoder.muse.module.knowledge.controller.app.muse.vo.AppKnowledgeBindingVO;
import cn.iocoder.muse.module.market.api.handoff.MarketHandoffTokenApi;
import cn.iocoder.muse.module.market.api.handoff.dto.HandoffConsumeReqDTO;
import cn.iocoder.muse.module.market.api.handoff.dto.HandoffVerifyReqDTO;
import cn.iocoder.muse.module.market.api.handoff.dto.HandoffVerifyRespDTO;
import cn.iocoder.muse.module.knowledge.dal.dataobject.muse.MuseKnowledgeBindPrecheckDO;
import cn.iocoder.muse.module.knowledge.dal.dataobject.muse.MuseKnowledgeBindingDO;
import cn.iocoder.muse.module.knowledge.dal.dataobject.muse.MuseKnowledgeCommandDO;
@ -61,6 +65,8 @@ public class MuseKnowledgeBindingService {
private MuseKnowledgeCommandService commandService;
@Resource
private MuseKnowledgeWorkOwnerFacade workOwnerFacade;
@Resource
private MarketHandoffTokenApi marketHandoffTokenApi;
@Transactional(rollbackFor = Exception.class)
public AppKnowledgeBindingVO.PrecheckRespVO createKnowledgeBindingPrecheck(Long loginUserId, String apiVersion,
@ -87,6 +93,11 @@ public class MuseKnowledgeBindingService {
AppKnowledgeBindingVO.ActionPolicyVO actionPolicy = actionPolicy(allowedPurposes, recheckReasons);
LocalDateTime expiresAt = LocalDateTime.now().plusMinutes(30);
String precheckId = "kb-bind-precheck-" + workId + "-" + safeCommandId(reqVO.getCommandId()) + "-" + UUID.randomUUID();
// market_kb 来源:兑现前服务端核验 + 核销 Market handoff token(替代"只存 hash 不验真"假安全;消费 token session)
// 沿用本方法 @Transactional 事务,verify/consume precheck 落库原子(失败一并回滚)token 明文仅 precheck 阶段持有
if ("market_kb".equals(reqVO.getSourceType())) {
consumeMarketHandoff(reqVO, apiVersion, workId, precheckId);
}
String sourceSnapshotId = "source-" + reqVO.getSourceType() + "-" + reqVO.getSourceId() + "-v" + reqVO.getSourceVersion();
Map<String, Object> summary = precheckSummary(reqVO, requestedPurposes, allowedPurposes, blockedPurposes,
blockedReasons, actionPolicy, sourceSnapshotId);
@ -255,6 +266,23 @@ public class MuseKnowledgeBindingService {
sourceBindingProjectionMapper.insert(projection);
}
/**
* market_kb 来源:服务端核验 handoff token(属主/未过期/未消费/未取消 + owner-action 匹配)并核销(消费换 session)
* 替代此前"只对 token 取 hash 存档、从不回 Market 验真"的假安全红线("不信任客户端 URL 参数")落地的关键一环
*/
private void consumeMarketHandoff(AppKnowledgeBindingVO.PrecheckReqVO reqVO, String apiVersion, Long workId,
String precheckId) {
HandoffVerifyRespDTO verify = marketHandoffTokenApi.verify(
new HandoffVerifyReqDTO(reqVO.getHandoffToken(), "knowledge", "bind"));
if (!verify.valid()) {
// token 不存在/非属主/已过期/已消费/owner-action 不匹配 阻断兑现,不写任何绑定事实
throw new ServiceException(KNOWLEDGE_MARKET_HANDOFF_UNAVAILABLE);
}
// 核销 token(pendingcompleted);沿用调用方 @Transactional 事务, precheck 落库原子
marketHandoffTokenApi.consume(new HandoffConsumeReqDTO(reqVO.getHandoffToken(), reqVO.getCommandId(),
apiVersion, verify.status(), workId, precheckId));
}
private void validatePrecheckReq(AppKnowledgeBindingVO.PrecheckReqVO reqVO) {
if (!SOURCE_TYPES.contains(reqVO.getSourceType())) {
throw new ServiceException(KNOWLEDGE_INVALID_PAGE_PARAM);