fix(security): F01 授权门禁不可被配置关闭 + F02 业务坐标按可信来源隔离(含唯一键 SQL)
build / build-push (push) Failing after 1m1s

This commit is contained in:
chenxing
2026-10-28 11:46:20 +08:00
parent 302216a07c
commit 8a1294aef2
17 changed files with 366 additions and 211 deletions
@@ -145,8 +145,8 @@ public class OapiFlowService {
FlowDefModel.parse(objectMapper, def.getContentJson()), formDataJson);
}
private FlowInstanceService.GuardedOutcome inTransaction(
org.springframework.transaction.support.TransactionCallback<FlowInstanceService.GuardedOutcome> action) {
private <T> T inTransaction(
org.springframework.transaction.support.TransactionCallback<T> action) {
return transactionTemplate == null ? action.doInTransaction(null) : transactionTemplate.execute(action);
}
@@ -229,7 +229,6 @@ public class OapiFlowService {
/** 代发起(biz uk 幂等 + R4 重启重提,见 FlowInstanceService):appKey=验签上下文调用方身份,
* 与 app_code 一起快照进办件(消息 jump_app 的权威来源,2026-10-12 起替代 bizType 冒充) */
@Transactional
public OapiModels.ApplyResultVo apply(Long schId, String appKey, OapiModels.ApplyBody body) {
requireLegacyWriter();
if (schId == null || body.getFlowGuid() == null || body.getBizType() == null
@@ -241,7 +240,8 @@ public class OapiFlowService {
String cleanAppKey = appKey == null || appKey.trim().isEmpty() ? null : appKey.trim();
// T01 使用许可(v1 路径):回执幂等(既有在途/通过件)不拦;无实例或终态件 R4 重启=新申报,
// 必须持有效使用许可。恢复感知的分支判定与 FlowInstanceService.apply 保持同键同序。
requireUsageGrantLegacyApply(schId, body.getFlowGuid(), cleanAppKey, body.getBizType(), body.getBizId());
FlowSourcePrincipalResolver.Principal source = requireApplySource(schId, cleanAppKey);
requireUsageGrant(schId, body.getFlowGuid(), source);
FlwDef def = requirePublishedDefForApply(schId, body.getFlowGuid());
// T06 变量终审:EXPRESSION 引用的业务变量缺失/空=null 即拒(零实例),不等运行期 nil 放行
requireExpressionVariablesPresent(def, asFormDataString(body.getFormData()));
@@ -260,8 +260,15 @@ public class OapiFlowService {
body.getBizUrl(), asSnapshotString(body.getBizSnapshot()),
body.getSubmitterId(), body.getSubmitterName(),
asFormDataString(body.getFormData()),
cleanAppKey, resolveApplyAppCode(cleanAppKey), assignJson);
FlowInstanceService.ApplyOutcome outcome = instanceService.apply(cmd);
cleanAppKey, source.appCode, assignJson);
cmd.sourcePrincipalId = source.id;
cmd.payloadHash = payloadHash(cmd);
FlowInstanceService.ApplyOutcome outcome;
try { outcome = inTransaction(tx -> instanceService.apply(cmd)); }
catch (DataIntegrityViolationException concurrent) {
// Outside the failed transaction: retry the SAME source-scoped lookup, never a broad business-key read.
outcome = inTransaction(tx -> instanceService.apply(cmd));
}
OapiModels.ApplyResultVo vo = new OapiModels.ApplyResultVo();
vo.setInstanceId(outcome.getInstanceId());
vo.setDuplicated(outcome.isDuplicated());
@@ -295,15 +302,9 @@ public class OapiFlowService {
}
requireSchoolMember(schId, body.getSubmitterId());
String cleanAppKey = appKey == null || appKey.trim().isEmpty() ? null : appKey.trim();
// T01 使用许可(v2 路径):恢复感知——SAME_REQUEST/REPLAY_ONLY/IDEMPOTENT/STALE_ROUND
// 四类回执分支不拦(已受理命令的回执不因许可撤销丢失);CREATED/RESTARTED(新申报/新轮)
// 必须持有效使用许可。分支判定与 FlowInstanceService.applyGuarded 同键同序;
// 完整命令协议(requestId/指纹/服务端轮次)统一归 T04 重排,此处不重构。
requireUsageGrantGuardedApply(schId, body.getFlowGuid(), cleanAppKey, body.getBizType(),
body.getBizId(), body.getRequestToken(), body.getAttempt(),
Boolean.TRUE.equals(body.getRetryOnly()));
FlwInstance existing = instanceRepository.findBySchIdAndBizTypeAndBizId(
schId, body.getBizType(), body.getBizId()).orElse(null);
FlowSourcePrincipalResolver.Principal source = requireApplySource(schId, cleanAppKey);
FlwInstance existing = instanceRepository.findBySchIdAndSourcePrincipalIdAndBizTypeAndBizIdAndLegacySlot(
schId, source.id, body.getBizType(), body.getBizId(), 1).orElse(null);
boolean replay = existing != null && body.getRequestToken() != null
&& !body.getRequestToken().trim().isEmpty()
&& body.getRequestToken().trim().equals(existing.getApplyRequestToken());
@@ -327,9 +328,15 @@ public class OapiFlowService {
body.getBizUrl(), asSnapshotString(body.getBizSnapshot()),
body.getSubmitterId(), body.getSubmitterName(),
asFormDataString(body.getFormData()),
cleanAppKey, resolveApplyAppCode(cleanAppKey), assignJson);
cleanAppKey, source.appCode, assignJson);
cmd.sourcePrincipalId = source.id;
// T04:受理载荷指纹(服务端计算,不信任客户端自报)——同 token 异载荷按冲突拒绝
cmd.payloadHash = payloadHash(cmd);
// Only an exact, source-owned, already accepted command can recover after revocation.
boolean provenReceipt = replay && source.id.equals(existing.getSourcePrincipalId())
&& existing.getApplyPayloadHash() != null && existing.getApplyPayloadHash().equals(cmd.payloadHash)
&& java.util.Objects.equals(existing.getSubmitterId(), cmd.submitterId);
if (!provenReceipt) requireUsageGrant(schId, body.getFlowGuid(), source);
FlowInstanceService.ApplyRetryPolicy policy = new FlowInstanceService.ApplyRetryPolicy(
body.getRequestToken(), body.getAttempt(), Boolean.TRUE.equals(body.getRetryOnly()),
body.getExpectedPrevRound());
@@ -379,37 +386,18 @@ public class OapiFlowService {
/**
* T01 使用许可闸·v1 路径(恢复感知最小形态):v1 无 token/轮次,回执与新申报的区分只能靠
* 既有实例状态——在途(10)/通过(20)命中=幂等回读放行;无实例=CREATED;终态(30/40)=R4
* 重启即新一轮申报。guard 未装配或未 enforce 时零行为(灰度开关,见 {@link FlowUsageGrantGuard})。
* 重启即新一轮申报。每次均强制通过独立的学校开通与流程使用授权校验。
*/
private void requireUsageGrantLegacyApply(Long schId, String flowGuid, String appKey,
String bizType, String bizId) {
if (usageGrantGuard == null || !usageGrantGuard.isEnforce() || appKey == null) {
return;
}
FlwInstance existing = instanceRepository
.findBySchIdAndBizTypeAndBizId(schId, bizType, bizId).orElse(null);
if (!legacyApplyIsReceiptOnly(existing)) {
usageGrantGuard.requireNewApplyGrant(schId, flowGuid, appKey);
private FlowSourcePrincipalResolver.Principal requireApplySource(Long school, String appKey) {
if (school == null || school <= 0 || sourcePrincipalResolver == null) {
throw new BizException(ErrorCode.FORBIDDEN, "缺少有效申报学校或来源解析服务");
}
return sourcePrincipalResolver.resolve(appKey);
}
/**
* T01 使用许可闸·v2 路径:与 {@code FlowInstanceService.applyGuarded/settleExisting}
* 同键同序的分支预判(判定逻辑见 {@link #guardedApplyIsReceiptOnly});
* 新申报(CREATED/RESTARTED)必须过 {@link FlowUsageGrantGuard#requireNewApplyGrant}。
* 完整命令协议统一归 T04。
*/
private void requireUsageGrantGuardedApply(Long schId, String flowGuid, String appKey,
String bizType, String bizId,
String requestToken, Integer attempt, boolean retryOnly) {
if (usageGrantGuard == null || !usageGrantGuard.isEnforce() || appKey == null) {
return;
}
FlwInstance existing = instanceRepository
.findBySchIdAndBizTypeAndBizId(schId, bizType, bizId).orElse(null);
if (!guardedApplyIsReceiptOnly(existing, requestToken, attempt, retryOnly)) {
usageGrantGuard.requireNewApplyGrant(schId, flowGuid, appKey);
}
private void requireUsageGrant(Long school, String flow, FlowSourcePrincipalResolver.Principal source) {
if (usageGrantGuard == null) throw new BizException(ErrorCode.FORBIDDEN, "流程使用授权服务未装配");
usageGrantGuard.requireNewApplyGrant(school, flow, source);
}
/**
@@ -497,39 +485,18 @@ public class OapiFlowService {
private FlowApplyRetryModels.ApplyResultV2 fromConcurrent(Long schId,
FlowInstanceService.ApplyCommand cmd,
FlowInstanceService.ApplyRetryPolicy policy) {
FlwInstance existing = instanceRepository.findBySchIdAndBizTypeAndBizId(
schId, cmd.bizType, cmd.bizId).orElse(null);
if (existing == null) {
throw new BizException(ErrorCode.CUSTOM, "办件创建冲突且读不到既有实例,请稍后重试");
}
if (existing.getApplyAppKey() != null && cmd.applyAppKey != null
&& !existing.getApplyAppKey().equals(cmd.applyAppKey)) {
throw new BizException(ErrorCode.FORBIDDEN,
"该业务键的办件属于另一调用应用(bizType=" + cmd.bizType + " bizId=" + cmd.bizId + ")");
}
if (existing.getSubmitterId() != null && cmd.submitterId != null
&& !existing.getSubmitterId().equals(cmd.submitterId)) {
throw new BizException(ErrorCode.FORBIDDEN, "该业务键的办件提交者与本次请求不符");
}
String token = policy.requestToken == null ? null : policy.requestToken.trim();
FlowInstanceService.GuardedOutcome outcome = inTransaction(tx -> instanceService.applyGuarded(cmd, policy));
requireNotConflict(outcome);
FlowApplyRetryModels.ApplyResultV2 vo = new FlowApplyRetryModels.ApplyResultV2();
vo.setInstanceId(existing.getId());
vo.setDuplicated(true);
vo.setApplyRound(existing.getApplyRound());
vo.setStatus(existing.getStatus());
vo.setVersion(existing.getVersion());
if (token != null && !token.equals(existing.getApplyRequestToken())) {
// T04:并发撞键且 token 不符=两份不同命令抢同一业务键——后到者冲突,不伪装幂等成功
//(旧 CONCURRENT_CREATED 分支只保留给无 token 的存量调用方)。
log.warn("[flow-center] 并发建件 token 不符(冲突拒绝):instance={} biz={}/{}",
existing.getId(), cmd.bizType, cmd.bizId);
throw new BizException(ErrorCode.CUSTOM,
"该业务键已被另一并发命令创建(请求标识不符),请按实例查询恢复,不要换 ID 重发");
}
vo.setBranch(token != null ? FlowApplyRetryModels.Branch.SAME_REQUEST
: FlowApplyRetryModels.Branch.CONCURRENT_CREATED);
log.warn("[flow-center] 并发建件命中既有实例:instance={} biz={}/{} branch={}",
existing.getId(), cmd.bizType, cmd.bizId, vo.getBranch());
vo.setInstanceId(outcome.instanceId); vo.setDuplicated(outcome.duplicated);
vo.setBranch(outcome.branch); vo.setApplyRound(outcome.applyRound);
instanceRepository.findById(outcome.instanceId).ifPresent(current -> {
int currentRound = current.getApplyRound() == null ? 1 : current.getApplyRound();
int outcomeRound = outcome.applyRound == null ? 1 : outcome.applyRound;
if (currentRound == outcomeRound && cmd.sourcePrincipalId.equals(current.getSourcePrincipalId())) {
vo.setStatus(current.getStatus()); vo.setVersion(current.getVersion());
}
});
return vo;
}
@@ -1087,9 +1054,8 @@ public class OapiFlowService {
/** 进度/换图共用的办件定位(schId 收敛在次) */
private java.util.Optional<FlwInstance> findByBizForOapi(Long school, String type, String biz) {
return formalCommandEnabled
? instanceRepository.findFirstBySchIdAndSourcePrincipalIdAndBizTypeAndBizIdOrderByApplyRoundDesc(school, FlowOapiSourceScope.current(), type, biz)
: instanceRepository.findBySchIdAndBizTypeAndBizId(school, type, biz);
return instanceRepository.findFirstBySchIdAndSourcePrincipalIdAndBizTypeAndBizIdOrderByApplyRoundDesc(
school, FlowOapiSourceScope.current(), type, biz);
}
private FlwInstance requireInstance(Long schId, Long instanceId) {
@@ -1356,12 +1322,16 @@ public class OapiFlowService {
reg.setAppKey(appKey);
reg.setFlowGuid(body.getFlowGuid());
}
if (formalCommandEnabled) {
{
FlowSourcePrincipalResolver.Principal source = sourcePrincipalResolver.resolve(appKey);
com.yunhai.flowcenter.entity.FlwUsageGrant grant = formalGrants.findBySchIdAndFlowGuidAndAppId(schId, body.getFlowGuid(), source.appId).orElse(null);
boolean permitted = grant != null && Integer.valueOf(1).equals(grant.getEnabled()) && source.id.equals(grant.getSourcePrincipalId());
if (!permitted && !formalCommands.existsBySchIdAndSourcePrincipalIdAndFlowGuidAndState(schId, source.id, body.getFlowGuid(), "ACCEPTED")) {
throw new BizException(ErrorCode.FORBIDDEN, "没有 SELF 订阅权限");
if (formalCommandEnabled) {
com.yunhai.flowcenter.entity.FlwUsageGrant grant = formalGrants.findBySchIdAndFlowGuidAndAppId(schId, body.getFlowGuid(), source.appId).orElse(null);
boolean permitted = grant != null && Integer.valueOf(1).equals(grant.getEnabled()) && source.id.equals(grant.getSourcePrincipalId());
if (!permitted && !formalCommands.existsBySchIdAndSourcePrincipalIdAndFlowGuidAndState(schId, source.id, body.getFlowGuid(), "ACCEPTED")) {
throw new BizException(ErrorCode.FORBIDDEN, "没有 SELF 订阅权限");
}
} else {
requireUsageGrant(schId, body.getFlowGuid(), source);
}
reg.setSourcePrincipalId(source.id);
}