소스 정보
- 저장소
- doccker/cc-use-exp
- 최근 소스 활동
- 2026년 4월 22일 12:42
- 감지된 SKILL.md 언어
- 중국어
- 스타
- 1,006
- 포크
- 103
설치 방법
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
소스 파일 검토
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
메뉴
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
직접 명령은 검토 Prompt를 거치지 않습니다. 실행하기 전에 소스를 확인하세요.
npx skills add https://github.com/doccker/cc-use-exp --skill cc-payment-callback-safety명령은 한 줄로 유지됩니다. 복사하기 전에 가로로 스크롤해 전체 내용을 확인하세요.
로컬 사본을 원하시나요? SkillsMP에서 현재 제공할 수 있는 파일을 다운로드하세요.
SOC 직업 분류 기준
SKILL.md 표시 중
| name | cc-payment-callback-safety |
| description | 支付回调/webhook 安全检查,防止回调伪造、重放攻击和金额篡改。 |
当系统涉及第三方支付回调(微信支付、支付宝等)或 webhook 通知时,防止伪造、篡改和重放攻击。
场景: 收到支付回调后直接解密/解析并处理,未验证请求确实来自支付平台
支付回调地址暴露在公网,任何人都可以伪造请求。如果不验签,攻击者可以构造假的"支付成功"通知。
// ❌ 错误: 直接解密处理,未验签
@PostMapping("/payment/callback/wechat")
public String wechatCallback(@RequestBody String body) {
JsonNode json = objectMapper.readTree(body);
// 直接尝试解密 resource
String decrypted = decryptAesGcm(ciphertext, apiV3Key, nonce, aad);
processPayment(decrypted); // 谁发来的都处理
return "{\"code\":\"SUCCESS\"}";
}
// ✅ 正确: 先验签,再解密,再处理
@PostMapping("/payment/callback/wechat")
public String wechatCallback(
@RequestBody String body,
@RequestHeader("Wechatpay-Signature") String signature,
@RequestHeader("Wechatpay-Timestamp") String timestamp,
@RequestHeader("Wechatpay-Nonce") String nonce,
@RequestHeader("Wechatpay-Serial") String serial) {
// 1. 用微信支付平台公钥验签(不是商户私钥)
String message = timestamp + "\n" + nonce + "\n" + body + "\n";
PublicKey platformKey = loadPlatformPublicKey(serial);
if (!verifySignature(message, signature, platformKey)) {
return "{\"code\":\"FAIL\",\"message\":\"验签失败\"}";
}
// 2. 验签通过后再解密和处理
String decrypted = decryptAesGcm(ciphertext, apiV3Key, nonce, aad);
processPayment(decrypted);
return "{\"code\":\"SUCCESS\"}";
}
| 密钥/证书 | 用途 | 持有方 |
|---|---|---|
| 商户私钥 | 商户请求微信时签名 | 商户 |
| 商户 API 证书 | 商户身份标识 | 商户 |
| 微信支付平台公钥/证书 | 验证微信回调签名 | 微信平台签发,商户持有公钥 |
| API V3 Key | 解密回调通知体 | 商户 |
Wechatpay-Serial 匹配对应平台证书(支持证书轮换)场景: 攻击者截获一份真实的支付成功回调,重复发送给系统
合法的旧回调报文签名仍然有效,仅靠验签无法防止重放。
// ❌ 错误: 验签通过就直接处理,不检查是否重复
if (verifySignature(message, signature, platformKey)) {
processPayment(decrypted); // 同一笔单可能被处理多次
}
// ✅ 正确: 三层防重放
// 第 1 层: 时间戳窗口
long callbackTime = Long.parseLong(timestamp);
long now = Instant.now().getEpochSecond();
if (Math.abs(now - callbackTime) > 300) { // 5 分钟窗口
return "{\"code\":\"FAIL\",\"message\":\"timestamp expired\"}";
}
// 第 2 层: 按 transaction_id 幂等
String transactionId = payData.get("transaction_id").asText();
if (paymentRecordRepository.existsByTransactionIdAndStatus(
transactionId, "SUCCESS")) {
log.info("重复通知,已处理: transactionId={}", transactionId);
return "{\"code\":\"SUCCESS\"}"; // 返回成功,让平台停止重试
}
// 第 3 层: 订单状态机幂等
TradeOrder order = orderRepository.findByOrderNumber(outTradeNo);
if (order.getPaymentStatus() == PaymentStatus.PAID) {
log.info("订单已支付,跳过: orderId={}", order.getId());
return "{\"code\":\"SUCCESS\"}";
}
场景: 直接使用回调报文中的金额作为入账依据,未与本地订单金额校验
即使验签通过,也应以本地订单金额为准做一致性校验,防止订单错配或极端情况下的金额不一致。
// ❌ 错误: 直接用回调金额入账
int paidAmount = payData.get("amount").get("total").asInt();
order.setPaidAmount(paidAmount); // 不校验是否与下单金额一致
order.setStatus("PAID");
// ✅ 正确: 回调金额必须与本地订单金额严格一致
int callbackAmountFen = payData.get("amount").get("total").asInt();
int expectedAmountFen = order.getTotalAmount()
.multiply(BigDecimal.valueOf(100)).intValue();
if (callbackAmountFen != expectedAmountFen) {
log.error("金额不一致: callback={}, expected={}, orderId={}",
callbackAmountFen, expectedAmountFen, order.getId());
// 标记异常,不入账
paymentRecord.setReconcileStatus("MISMATCHED");
paymentRecord.setReconcileErrorMessage("金额不一致");
return;
}
场景: 回调链路有完整校验,但"手动对账"链路跳过了部分校验
对账补单和回调处理本质上都是"确认支付成功",如果走不同的校验逻辑,容易在对账链路留下安全缺口。
// ❌ 错误: 回调有完整校验
public void handleCallback(JsonNode payData, TradeOrder order) {
validateAmount(payData, order);
validateMchId(payData, config);
validateAppId(payData, config);
processPayment(order, transactionId);
}
// ❌ 错误: 对账直接补单,跳过校验
public void reconcile(PaymentRecord record) {
JsonNode queryResult = queryWechatOrder(outTradeNo);
if ("SUCCESS".equals(tradeState)) {
processPayment(order, transactionId); // 没有校验金额/商户号
}
}
// ✅ 正确: 抽取统一校验函数,回调和对账都复用
public void validatePaymentResult(
JsonNode payData, TradeOrder order, TenantConfig config) {
// 1. out_trade_no 一致
// 2. transaction_id 非空
// 3. appid 一致
// 4. mchid 一致
// 5. 金额一致
// 任何一项不通过都抛异常
}
// 回调链路
public void handleCallback(JsonNode payData, TradeOrder order, TenantConfig config) {
validatePaymentResult(payData, order, config);
processPayment(order, transactionId);
}
// 对账链路
public void reconcile(PaymentRecord record) {
JsonNode queryResult = queryWechatOrder(outTradeNo);
validatePaymentResult(queryResult, order, config); // 同一套校验
processPayment(order, transactionId);
}
场景: 支付查询、对账等关键逻辑使用占位实现,但未被发现就上线了
// ❌ 错误: 占位实现,返回假数据
private JsonNode queryWechatOrderStatus(PaymentRecord record) {
// TODO: 接入微信支付订单查询 V3 API
log.warn("微信订单查询 API 待实现: orderId={}", record.getOrderId());
return objectMapper.createObjectNode()
.put("trade_state", "NOTPAY")
.put("trade_state_desc", "未支付(占位)");
}
// ✅ 正确: 占位实现必须明确失败,不能返回看似正常的假数据
private JsonNode queryWechatOrderStatus(PaymentRecord record) {
throw new UnsupportedOperationException(
"微信订单查询 API 未实现,请先完成接入");
}
验签与防伪:
防重放:
业务校验:
实现完整性:
> 📋 本回复遵循:`cc-payment-callback-safety` - [章节名]