fix(security): F01 授权门禁不可被配置关闭 + F02 业务坐标按可信来源隔离(含唯一键 SQL)
build / build-push (push) Failing after 1m1s
build / build-push (push) Failing after 1m1s
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user