Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/*
Expand Down Expand Up @@ -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()){

// 获取支付参数
Expand All @@ -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{

Expand All @@ -93,6 +98,7 @@ public ChannelRetMsg doNotice(HttpServletRequest request, Object params, PayOrde
alipaySignType = alipayParams.getSignType();
alipayPublicCert = alipayParams.getAlipayPublicCert();
alipayPublicKey = alipayParams.getAlipayPublicKey();
expectedAppId = alipayParams.getAppId();
}

// 获取请求参数
Expand All @@ -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");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/*
* 【支付宝】支付通道工具包
Expand Down Expand Up @@ -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<String, String> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String, String> 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){
Expand All @@ -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<String, String> 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);
Expand Down
Loading