feat(risk): 接入海外IP地域拦截风控维度, 复用ip2region判定境外IP, 新增overseas_ip命中类型, 受风控总开关门控

This commit is contained in:
daxpay
2026-07-31 20:58:14 +08:00
parent db247050b8
commit 523289e6d0
16 changed files with 82 additions and 11 deletions

View File

@@ -57,4 +57,10 @@ public class PayRiskCheckContext {
/// 事前命中是否阻断下单null/true=阻断false=仅落命中记录)
private Boolean blockOnHit;
/// 是否拦截海外 IPnull/false=不拦截true=拦截)
///
/// 地域策略开关快照, 由 [cn.daxpay.open.payment.trade.runtime.service.pay.common.PayRiskAssistService]
/// 读取平台配置后注入, 供检查器按 IP 归属地判定是否命中。
private Boolean blockOverseasIp;
}

View File

@@ -51,6 +51,8 @@ public class PayRiskAssistService {
PayRiskCheckContext ctx = buildContextFromParam(payParam, scene);
// false=仅记录不拦截;缺省/true=命中拒绝下单
ctx.setBlockOnHit(!Boolean.FALSE.equals(config.getRiskBlockBeforePay()));
// 海外 IP 拦截开关(地域策略)
ctx.setBlockOverseasIp(config.getBlockOverseasIp());
checker.checkBeforePay(ctx);
}
@@ -68,6 +70,8 @@ public class PayRiskAssistService {
return;
}
PayRiskCheckContext ctx = buildContextFromTrade(trade);
// 海外 IP 拦截开关(地域策略, 事后仅记录海外访问)
ctx.setBlockOverseasIp(config.getBlockOverseasIp());
if (StrUtil.isNotBlank(buyerId)) {
ctx.setBuyerId(buyerId);
}

View File

@@ -1,5 +1,6 @@
{
"ip": "IP",
"alipay_user": "Alipay user",
"wechat_openid": "WeChat OpenId"
"wechat_openid": "WeChat OpenId",
"overseas_ip": "Overseas IP"
}

View File

@@ -1,5 +1,6 @@
{
"ip": "IP",
"alipay_user": "Pengguna Alipay",
"wechat_openid": "OpenId WeChat"
"wechat_openid": "OpenId WeChat",
"overseas_ip": "IP Luar Negeri"
}

View File

@@ -1,5 +1,6 @@
{
"ip": "IP",
"alipay_user": "Alipay ユーザー",
"wechat_openid": "WeChat OpenId"
"wechat_openid": "WeChat OpenId",
"overseas_ip": "海外IP"
}

View File

@@ -1,5 +1,6 @@
{
"ip": "IP",
"alipay_user": "알리페이 사용자",
"wechat_openid": "위챗 OpenId"
"wechat_openid": "위챗 OpenId",
"overseas_ip": "해외 IP"
}

View File

@@ -1,5 +1,6 @@
{
"ip": "IP",
"alipay_user": "Pengguna Alipay",
"wechat_openid": "OpenId WeChat"
"wechat_openid": "OpenId WeChat",
"overseas_ip": "IP Luar Negara"
}

View File

@@ -1,5 +1,6 @@
{
"ip": "IP",
"alipay_user": "ผู้ใช้ Alipay",
"wechat_openid": "OpenId WeChat"
"wechat_openid": "OpenId WeChat",
"overseas_ip": "IP ต่างประเทศ"
}

View File

@@ -1,5 +1,6 @@
{
"ip": "IP",
"alipay_user": "Người dùng Alipay",
"wechat_openid": "OpenId WeChat"
"wechat_openid": "OpenId WeChat",
"overseas_ip": "IP nước ngoài"
}

View File

@@ -1,5 +1,6 @@
{
"ip": "IP",
"alipay_user": "支付宝用户",
"wechat_openid": "微信 OpenId"
"wechat_openid": "微信 OpenId",
"overseas_ip": "海外IP"
}

View File

@@ -1,5 +1,6 @@
{
"ip": "IP",
"alipay_user": "支付寶用戶",
"wechat_openid": "微信 OpenId"
"wechat_openid": "微信 OpenId",
"overseas_ip": "海外IP"
}

View File

@@ -1,5 +1,6 @@
{
"ip": "IP",
"alipay_user": "支付寶用戶",
"wechat_openid": "微信 OpenId"
"wechat_openid": "微信 OpenId",
"overseas_ip": "海外IP"
}

View File

@@ -11,4 +11,13 @@
<artifactId>daxpay-plugin-risk</artifactId>
<packaging>jar</packaging>
<description>支付风控插件(黑名单配置 + 命中记录)</description>
<dependencies>
<!-- 审计日志模块IP 归属地查询服务, 海外 IP 拦截依赖) -->
<dependency>
<groupId>cn.daxpay.open</groupId>
<artifactId>capability-audit-log</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>

View File

@@ -18,7 +18,9 @@ public enum PayBlacklistTypeEnum implements I18nSupport {
/// 支付宝用户userId通道内全局
ALIPAY_USER("alipay_user"),
/// 微信 OpenId绑定平台支付应用
WECHAT_OPENID("wechat_openid");
WECHAT_OPENID("wechat_openid"),
/// 海外 IP地域策略命中, 非黑名单来源)
OVERSEAS_IP("overseas_ip");
private final String code;

View File

@@ -166,6 +166,10 @@ public class PayBlacklistService {
/// 将命中快照映射为名单 type + wxAppId微信缺 AppId 则返回 null
private ResolvedIdentity resolveIdentity(String hitType, String channel, String wxAppId) {
// 海外 IP 命中加黑: 映射为普通 IP 黑名单(持续拉黑该具体 IP
if (PayBlacklistTypeEnum.OVERSEAS_IP.getCode().equals(hitType)) {
return new ResolvedIdentity(PayBlacklistTypeEnum.IP.getCode(), null);
}
if (PayBlacklistTypeEnum.IP.getCode().equals(hitType)) {
return new ResolvedIdentity(PayBlacklistTypeEnum.IP.getCode(), null);
}

View File

@@ -4,6 +4,8 @@ import cn.daxpay.open.payment.strategy.risk.PayRiskCheckContext;
import cn.daxpay.open.payment.strategy.risk.PayRiskChecker;
import cn.daxpay.open.platform.core.code.PayErrorCode;
import cn.daxpay.open.platform.core.enums.pay.channel.ChannelEnum;
import cn.daxpay.open.platform.capability.audit.log.service.ip2region.IpRegion;
import cn.daxpay.open.platform.capability.audit.log.service.ip2region.IpToRegionService;
import cn.daxpay.open.platform.core.exception.BizInfoException;
import cn.daxpay.open.plugin.risk.entity.PayBlacklist;
import cn.daxpay.open.plugin.risk.enums.PayBlacklistTypeEnum;
@@ -31,6 +33,7 @@ public class DefaultPayRiskChecker implements PayRiskChecker {
private final PayBlacklistService payBlacklistService;
private final PayRiskHitService payRiskHitService;
private final IpToRegionService ipToRegionService;
/// 用户标识黑名单存在性缓存(短 TTL 30s
///
@@ -60,6 +63,10 @@ public class DefaultPayRiskChecker implements PayRiskChecker {
+ "tradeType={}, method={}, mchNo={}, clientIp={}",
ctx.getTradeType(), ctx.getMethod(), ctx.getMchNo(), ctx.getClientIp());
}
// 海外 IP 拦截(地域策略, 非黑名单)
if (Boolean.TRUE.equals(ctx.getBlockOverseasIp())) {
checkOverseasIp(ctx, throwOnHit);
}
}
@Override
@@ -75,6 +82,10 @@ public class DefaultPayRiskChecker implements PayRiskChecker {
if (StrUtil.isNotBlank(ctx.getBuyerId()) && !StrUtil.equals(ctx.getBuyerId(), ctx.getOpenId())) {
checkUserIdentity(ctx, ctx.getBuyerId(), false);
}
// 海外 IP 访问记录(事后仅记录, 不阻断)
if (Boolean.TRUE.equals(ctx.getBlockOverseasIp())) {
checkOverseasIp(ctx, false);
}
}
@Override
@@ -127,4 +138,29 @@ public class DefaultPayRiskChecker implements PayRiskChecker {
}
return true;
}
/// 海外 IP 地域拦截country≠中国, 港澳台放行; 未知/内网 fail-open
///
/// 与黑名单不同, 海外命中不关联名单行blacklistId=null, 命中类型为 [PayBlacklistTypeEnum#OVERSEAS_IP]。
private void checkOverseasIp(PayRiskCheckContext ctx, boolean throwOnHit) {
String ip = ctx.getClientIp();
if (StrUtil.isBlank(ip)) {
return;
}
IpRegion region = ipToRegionService.getRegionByIp(ip);
// 未知IPv6/查询失败)/ 内网 / 国内(含港澳台) → 放行
if (region == null || region.isInnerIp() || region.isChinaIp()) {
return;
}
// 海外 → 命中记录blacklistId=null, 非黑名单来源)
try {
payRiskHitService.recordHit(ctx, PayBlacklistTypeEnum.OVERSEAS_IP.getCode(), ip, null);
} catch (Exception e) {
log.warn("记录海外IP命中失败 ip={}: {}", ip, e.getMessage());
}
if (throwOnHit) {
// 交易被限制(模糊文案,防探测)
throw new BizInfoException(PayErrorCode.OPERATION_FAIL, "pay.error.risk.blacklist");
}
}
}