diff --git a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/alipay/AlipayChannelNoticeService.java b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/alipay/AlipayChannelNoticeService.java index a37dd6459..bcfbbbb87 100644 --- a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/alipay/AlipayChannelNoticeService.java +++ b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/alipay/AlipayChannelNoticeService.java @@ -23,15 +23,18 @@ import com.jeequan.jeepay.core.model.params.alipay.AlipayConfig; import com.jeequan.jeepay.core.model.params.alipay.AlipayIsvParams; import com.jeequan.jeepay.core.model.params.alipay.AlipayNormalMchParams; +import com.jeequan.jeepay.core.utils.AmountUtil; import com.jeequan.jeepay.pay.channel.AbstractChannelNoticeService; import com.jeequan.jeepay.pay.model.MchAppConfigContext; import com.jeequan.jeepay.pay.rqrs.msg.ChannelRetMsg; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.MutablePair; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import jakarta.servlet.http.HttpServletRequest; +import java.math.BigDecimal; import java.util.Map; /* @@ -75,6 +78,7 @@ public ChannelRetMsg doNotice(HttpServletRequest request, Object params, PayOrde //配置参数获取 Byte useCert = null; String alipaySignType, alipayPublicCert, alipayPublicKey = null; + String expectedAppId; // 业务字段交叉校验用 if(mchAppConfigContext.isIsvsubMch()){ // 获取支付参数 @@ -83,6 +87,7 @@ public ChannelRetMsg doNotice(HttpServletRequest request, Object params, PayOrde alipaySignType = alipayParams.getSignType(); alipayPublicCert = alipayParams.getAlipayPublicCert(); alipayPublicKey = alipayParams.getAlipayPublicKey(); + expectedAppId = alipayParams.getAppId(); }else{ @@ -93,6 +98,7 @@ public ChannelRetMsg doNotice(HttpServletRequest request, Object params, PayOrde alipaySignType = alipayParams.getSignType(); alipayPublicCert = alipayParams.getAlipayPublicCert(); alipayPublicKey = alipayParams.getAlipayPublicKey(); + expectedAppId = alipayParams.getAppId(); } // 获取请求参数 @@ -113,6 +119,33 @@ public ChannelRetMsg doNotice(HttpServletRequest request, Object params, PayOrde throw ResponseException.buildText("ERROR"); } + // 业务字段交叉校验:验签通过仅证明请求来自支付宝并由本商户密钥签名, + // 不保证回调内容就是这笔本地订单。多商户配置切换 / 跨订单签名穿越 / 沙箱与生产串扰 + // 等场景下,仍可能出现 out_trade_no、金额、app_id 与本地订单上下文不一致的回调。 + // 任一字段不一致 → 记 ERROR 日志 + 返回 SUCCESS 阻断支付宝重试 8 次 + 不更新订单状态。 + String notifyOutTradeNo = jsonParams.getString("out_trade_no"); + String notifyAppId = jsonParams.getString("app_id"); + String notifyTotalAmount = jsonParams.getString("total_amount"); + String expectedTotalAmount = AmountUtil.convertCent2Dollar(payOrder.getAmount()); + boolean amountMatch; + try { + amountMatch = new BigDecimal(notifyTotalAmount).compareTo(new BigDecimal(expectedTotalAmount)) == 0; + } catch (NumberFormatException e) { + amountMatch = false; + } + if (!StringUtils.equals(notifyOutTradeNo, payOrder.getPayOrderId()) + || !StringUtils.equals(notifyAppId, expectedAppId) + || !amountMatch) { + log.error("支付宝异步回调业务字段不匹配,已拒绝处理。out_trade_no=[{}] expected=[{}]; app_id=[{}] expected=[{}]; total_amount=[{}] expected=[{}]", + notifyOutTradeNo, payOrder.getPayOrderId(), + notifyAppId, expectedAppId, + notifyTotalAmount, expectedTotalAmount); + ChannelRetMsg dropped = new ChannelRetMsg(); + dropped.setResponseEntity(textResp("SUCCESS")); + dropped.setChannelState(ChannelRetMsg.ChannelState.UNKNOWN); + return dropped; + } + //验签成功后判断上游订单状态 ResponseEntity okResponse = textResp("SUCCESS"); diff --git a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/alipay/AlipayKit.java b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/alipay/AlipayKit.java index ea90a1a44..ba4f6f5fb 100644 --- a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/alipay/AlipayKit.java +++ b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/alipay/AlipayKit.java @@ -27,6 +27,8 @@ import com.jeequan.jeepay.pay.model.MchAppConfigContext; import com.jeequan.jeepay.pay.service.ConfigContextQueryService; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.Pair; /* * 【支付宝】支付通道工具包 @@ -108,6 +110,30 @@ public static String appendErrCode(String code, String subCode){ return StringUtils.defaultIfEmpty(subCode, code); //优先: subCode } + /** + * 解析支付宝授权回调中拼接的 state 参数(格式:ISVNO_MCHAPPID)。 + * + * 历史上 controller 直接写 split("_")[0]/[1],没做边界校验: + * 空串、不含 "_"、或者 "_" 在首尾的输入都会触发 ArrayIndexOutOfBoundsException + * 或得到空字符串,进而被恶意构造的请求触发回调链路 500。 + * + * 使用 indexOf 取第一个 "_" 切分,而不是 split,避免 mchAppId 内部含 "_" 时被切碎。 + * + * @return ImmutablePair(isvNo, mchAppId);解析失败返回 null,由调用方决定如何提示 + */ + public static Pair parseIsvAndMchAppIdState(String state) { + if (StringUtils.isEmpty(state)) { + return null; + } + int idx = state.indexOf('_'); + // idx <= 0:没有 "_" 或 "_" 在最前面(isvNo 为空) + // idx >= state.length() - 1:" _" 在最后(mchAppId 为空) + if (idx <= 0 || idx >= state.length() - 1) { + return null; + } + return ImmutablePair.of(state.substring(0, idx), state.substring(idx + 1)); + } + public static String appendErrMsg(String msg, String subMsg){ String result = null; diff --git a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/alipay/ctrl/AlipayBizController.java b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/alipay/ctrl/AlipayBizController.java index 54abec579..e00d4bb06 100644 --- a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/alipay/ctrl/AlipayBizController.java +++ b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/alipay/ctrl/AlipayBizController.java @@ -38,6 +38,7 @@ import com.jeequan.jeepay.service.impl.SysConfigService; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; @@ -73,10 +74,16 @@ public class AlipayBizController extends AbstractCtrl { @RequestMapping("/redirectAppToAppAuth/{isvAndMchAppId}") public void redirectAppToAppAuth(@PathVariable("isvAndMchAppId") String isvAndMchAppId) throws IOException { - String isvNo = isvAndMchAppId.split("_")[0]; + Pair parsed = AlipayKit.parseIsvAndMchAppIdState(isvAndMchAppId); + if (parsed == null) { + throw new BizException("授权参数非法:isvAndMchAppId 格式应为 ISVNO_MCHAPPID"); + } + String isvNo = parsed.getLeft(); AlipayIsvParams alipayIsvParams = (AlipayIsvParams) configContextQueryService.queryIsvParams(isvNo, CS.IF_CODE.ALIPAY); - alipayIsvParams.getSandbox(); + if (alipayIsvParams == null) { + throw new BizException("ISV [" + isvNo + "] 未配置支付宝参数"); + } String oauthUrl = AlipayConfig.PROD_APP_TO_APP_AUTH_URL; if(alipayIsvParams.getSandbox() != null && alipayIsvParams.getSandbox() == CS.YES){ @@ -101,10 +108,17 @@ public String appToAppAuthCallback() { if(StringUtils.isNotEmpty(isvAndMchAppId) && StringUtils.isNotEmpty(appAuthCode)){ isAlipaySysAuth = false; - String isvNo = isvAndMchAppId.split("_")[0]; - String mchAppId = isvAndMchAppId.split("_")[1]; + + Pair parsed = AlipayKit.parseIsvAndMchAppIdState(isvAndMchAppId); + if (parsed == null) { + throw new BizException("授权回调 state 格式非法:应为 ISVNO_MCHAPPID"); + } + String mchAppId = parsed.getRight(); MchApp mchApp = mchAppService.getById(mchAppId); + if (mchApp == null) { + throw new BizException("商户应用 [" + mchAppId + "] 不存在"); + } MchAppConfigContext mchAppConfigContext = configContextQueryService.queryMchInfoAndAppInfo(mchApp.getMchNo(), mchAppId); AlipayClientWrapper alipayClientWrapper = configContextQueryService.getAlipayClientWrapper(mchAppConfigContext);