ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Spring Boot实现Agent签名写网关

Spring Boot实现Agent签名写网关 前一篇讲了为什么 Agent 写数据库前应该先签名。这一篇直接落地一个最小 Spring Boot Write Gateway。目标链路Agent ↓ Signed Action ↓ Spring Boot Gateway ├─ Signature Verify ├─ Expiry ├─ Idempotency ├─ Authorization ├─ Business Rule └─ Audit ↓ Database重点不是把 KMS SDK 塞进 Controller而是把“状态变更”变成一个可验证的协议。第一步定义Action EnvelopepublicrecordSignedActionRequest(StringagentId,StringrunId,StringsubjectId,Stringpurpose,Stringcapability,StringresourceId,StringgrantId,StringapprovalId,StringidempotencyKey,InstantexpiresAt,JsonNodepayload,StringkeyId,StringkeyVersion,Stringsignature){}这里几个字段不能省agentId runId capability resourceId idempotencyKey expiresAt否则签名很难绑定到真正的业务动作。第二步Canonical Payload签名不能直接对 JavatoString()。定义publicinterfaceActionCanonicalizer{byte[]canonicalize(SignedActionRequestrequest);}实现时只把参与签名的字段放进去。ComponentpublicclassJsonActionCanonicalizerimplementsActionCanonicalizer{privatefinalObjectMappermapper;Overridepublicbyte[]canonicalize(SignedActionRequestrequest){ObjectNodenodemapper.createObjectNode();node.put(agent_id,request.agentId());node.put(run_id,request.runId());node.put(subject_id,request.subjectId());node.put(purpose,request.purpose());node.put(capability,request.capability());node.put(resource_id,request.resourceId());node.put(grant_id,request.grantId());node.put(approval_id,request.approvalId());node.put(idempotency_key,request.idempotencyKey());node.put(expires_at,request.expiresAt().toString());node.set(payload,request.payload());try{returnmapper.writer().with(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS).writeValueAsBytes(node);}catch(JsonProcessingExceptione){thrownewIllegalStateException(e);}}}生产环境建议使用真正的 JSON Canonicalization Scheme而不是只靠 Map 排序。第三步先算Action HashComponentpublicclassActionHasher{publicStringsha256(byte[]content){try{MessageDigestdigestMessageDigest.getInstance(SHA-256);returnHexFormat.of().formatHex(digest.digest(content));}catch(NoSuchAlgorithmExceptione){thrownewIllegalStateException(e);}}}这个 Hash 后面同时绑定签名 Approval Idempotency Audit不要各算各的。第四步Signature Verifier做接口publicinterfaceAgentSignatureVerifier{VerificationResultverify(StringkeyId,StringkeyVersion,byte[]canonicalPayload,Stringsignature);}这样生产可以接Cloud KMS HSM Vault Transit AWS KMS测试环境可以用本地公钥。本地RSA验证示例ComponentpublicclassRsaSignatureVerifierimplementsAgentSignatureVerifier{privatefinalAgentPublicKeyStorekeyStore;OverridepublicVerificationResultverify(StringkeyId,StringkeyVersion,byte[]payload,StringsignatureBase64){try{PublicKeykeykeyStore.load(keyId,keyVersion);SignaturesignatureSignature.getInstance(SHA256withRSA);signature.initVerify(key);signature.update(payload);booleanvalidsignature.verify(Base64.getDecoder().decode(signatureBase64));returnvalid?VerificationResult.valid():VerificationResult.invalid(SIGNATURE_MISMATCH);}catch(GeneralSecurityExceptione){returnVerificationResult.invalid(VERIFY_ERROR);}}}生产 Agent 不保存私钥。这里只加载 Public Key。第五步Expiry先于数据库写ComponentpublicclassActionExpiryValidator{privatefinalClockclock;publicvoidvalidate(SignedActionRequestrequest){if(request.expiresAt().isBefore(clock.instant())){thrownewActionExpiredException();}}}Clock 要注入。以后 Replay / Test 才能固定时间。第六步Idempotency必须数据库唯一EntityTable(nameagent_write_idempotency,uniqueConstraints{UniqueConstraint(columnNamesidempotencyKey)})publicclassIdempotencyEntity{IdprivateStringid;privateStringidempotencyKey;privateStringactionHash;Enumerated(EnumType.STRING)privateWriteStatusstatus;privateStringresultRef;privateInstantcreatedAt;}状态publicenumWriteStatus{STARTED,COMMITTED,FAILED,UNKNOWN}为什么要有UNKNOWN最危险窗口数据库提交成功 ↓ Gateway返回前进程Crash调用方重试。如果只看到timeout很容易重复写。所以如果结果无法确定UNKNOWN下一次调用必须Reconcile不能直接再次执行。第七步同Idempotency Key不同Hash要报警publicIdempotencyDecisioncheck(Stringkey,StringactionHash){OptionalIdempotencyEntityexistingrepository.findByIdempotencyKey(key);if(existing.isEmpty()){returnNEW;}if(!existing.get().getActionHash().equals(actionHash)){thrownewIdempotencyConflictException();}returnREPLAY;}这类冲突不应该返回409普通业务错误而应该进入安全审计。第八步Authorization Validator签名有效不代表有权写。publicinterfaceActionAuthorizationService{AuthorizationDecisionauthorize(SignedActionRequestrequest,StringactionHash);}至少检查Agent Identity Grant Purpose Capability Resource Ownership ApprovalApproval也绑定同一个HashpublicvoidrequireApproval(SignedActionRequestrequest,StringactionHash){if(request.approvalId()null){thrownewApprovalRequiredException();}ApprovalRecordapprovalapprovalRepository.load(request.approvalId());if(!approval.actionHash().equals(actionHash)){thrownewApprovalMismatchException();}}审批以后payload任何关键字段变化都会重新计算 Hash。旧 Approval 自动失效。第九步Business Rule最后再做例如退款ComponentpublicclassRefundBusinessPolicy{publicvoidvalidate(RefundPayloadpayload,Orderorder){if(payload.amountMinor()order.refundableMinor()){thrownewRefundLimitException();}if(order.status()OrderStatus.CLOSED){thrownewInvalidOrderStateException();}}}签名和授权都通过以后仍然必须过业务规则。第十步事务边界TransactionalpublicWriteResultexecute(SignedActionRequestrequest){byte[]canonicalcanonicalizer.canonicalize(request);StringactionHashhasher.sha256(canonical);expiryValidator.validate(request);signatureService.requireValid(request,canonical);authorizationService.requireAllowed(request,actionHash);IdempotencyDecisiondecisionidempotencyService.begin(request.idempotencyKey(),actionHash);if(decision.isReplay()){returnidempotencyService.loadResult(request.idempotencyKey());}WriteResultresultmutationService.execute(request);idempotencyService.markCommitted(request.idempotencyKey(),result);auditService.append(request,actionHash,result);returnresult;}如果 Audit 必须和业务写强一致可以Transactional Outbox不要直接同步发 Kafka。Audit EventpublicrecordSignedWriteAuditEvent(StringeventId,StringagentId,StringrunId,StringsubjectId,Stringcapability,StringresourceId,StringactionHash,StringkeyId,StringkeyVersion,StringgrantId,StringapprovalId,Stringresult,InstantoccurredAt){}不要把完整敏感 Payload 直接写日志。保存Hash Encrypted Evidence Ref第十一步Key RegistryEntitypublicclassAgentSigningKeyEntity{IdprivateStringkeyId;privateStringagentId;privateStringkeyVersion;privateStringpublicKeyRef;Enumerated(EnumType.STRING)privateKeyStatusstatus;privateInstantvalidFrom;privateInstantvalidUntil;}状态publicenumKeyStatus{ACTIVE,VERIFY_ONLY,REVOKED,EXPIRED}轮换后旧 KeyVERIFY_ONLY历史记录还能验证但不能再用于新签名。第十二步Agent和Key必须一一约束如果 Requestagent_idrefund-agent却使用research-agent-key必须拒绝。if(!key.agentId().equals(request.agentId())){thrownewAgentKeyMismatchException();}这是非常重要的身份绑定。第十三步Metricsagent_write_total{ capability, result } agent_signature_invalid_total{ reason } agent_idempotency_conflict_total agent_write_unknown_total agent_approval_mismatch_total agent_key_mismatch_total不要把run_id放 Metric Tag。Run 细节放 Trace。Traceagent.write ├─canonicalize ├─signature.verify ├─expiry.validate ├─authorization ├─idempotency ├─business.validate ├─mutation └─audit一条写操作到底卡在哪层一眼能看到。第十四步失败要分层publicenumWriteFailureCode{SIGNATURE_INVALID,KEY_REVOKED,ACTION_EXPIRED,AUTHORITY_DENIED,APPROVAL_MISMATCH,IDEMPOTENCY_CONFLICT,BUSINESS_RULE_DENIED,MUTATION_FAILED,RESULT_UNKNOWN}不要全部抛WRITE_FAILED这会让事故分析非常痛苦。第十五步最重要的单元测试TestvoidchangedPayloadMustInvalidateApproval(){SignedActionRequestrequestfixture.approvedRefund();StringoldHashfixture.approvalHash();SignedActionRequestchangedfixture.withAmount(request,99900L);assertNotEquals(oldHash,hash(changed));assertThrows(ApprovalMismatchException.class,()-gateway.execute(changed));}还要测这些签名被篡改 Payload被篡改 Key已撤销 Action过期 Agent-Key不匹配 相同Key重复请求 相同Key不同Hash Grant撤销 Approval Hash不一致 事务提交后响应丢失最后一个 Case 要配UNKNOWN Reconcile网关的部署边界不要让 Agent 同时拥有Write Gateway Direct DB否则所有检查都能绕过。网络策略Agent → Write Gateway允许。Agent → DB拒绝。只有 Gateway Service Account 有数据库写权限。Spring Boot 这层真正解决的不是“怎么验一个RSA签名”。它把生产 Agent 的写操作固化成一条确定性链谁 →签了什么 →基于什么授权 →有没有过期 →是否重复 →业务是否允许 →最终写了什么只要 Agent 可以改真实状态这条链就比 Prompt 里的任何“请谨慎操作”更重要。模型可以负责提出动作。真正决定能不能落库的应该一直是这层确定性网关。
返回列表