支付安全:大厂如何防止订单重复支付的揭秘_订单系统 防重复支付

在如今数字支付横行的时代,我们常常在购物、服务支付等场景中使用在线支付。然而,你可曾想过,背后的支付系统是如何确保你的订单不会被重复支付,保障交易安全的呢?本文将带你深入了解大厂是如何通过巧妙的设计和多层次的防护机制来避免订单重复支付的。

支付流程概览

首先,我们先来简单了解一下典型的支付流程。通常,当我们提交订单后,支付流程会经过以下关键步骤:

1,提交订单: 用户在商家平台上提交订单,生成支付请求。

2,支付网关: 订单经过支付网关,连接着第三方支付渠道(如微信、支付宝、银联等)。

3,支付中心交互: 支付中心与第三方支付渠道进行交互,完成支付。

4,支付结果通知: 支付中心异步接收支付结果,更新支付状态,并通知业务应用进行订单状态更新。

常见问题:订单掉单

在这一过程中,可能会面临一个棘手的问题,即订单掉单。无论是由于网络超时、程序错误,还是其他原因,都有可能导致支付成功但订单状态未更新,从而引发用户投诉或重复支付。

1,外部掉单和内部掉单:

a,外部掉单: 由提交订单到支付成功的过程中,可能出现超时未收到回调通知等问题。

b,内部掉单: 在支付成功后,由于支付中心或业务应用自身问题,未能及时更新订单状态。

防范措施:支付流水状态和超时处理

2,防止订单重复支付,可以采取一系列措施:
a,支付流水状态: 在支付订单中引入一个中间状态,“支付中”。在支付时,检查是否有相同订单状态为“支付中”的支付流水,使用锁确保支付的原子性。支付完成后,再将状态更新为“支付成功”。

b,超时处理: 设定支付中心的自定义超时时间,如30秒。若在规定时间内未收到支付成功回调,主动查询支付结果,以确保及时更新。可在10秒、20秒、30秒等时间点查询,超过最大查询次数则进行异常处理。

3,异步通知和接口幂等性
在支付结果通知阶段,需要注意以下两个重要方面:

异步通知: 支付中心收到支付结果后,将结果同步给业务系统。这可以通过消息队列(MQ)或直接调用实现,但直接调用需考虑重试机制,可以定义实现重试策略或借助现成框架实现如SpringBoot Retry。

接口幂等性: 无论是支付中心还是业务应用,在接收支付结果通知时都要保证接口的幂等性,即同一消息只处理一次,忽略其余的重复通知。

4,业务应用的主动查询和超时处理
为了更全面地确保支付安全,业务应用也应主动参与:

a,超时主动查询: 发起支付时,将支付订单放入一张表中,通过定时任务定期扫描并查询支付结果。这确保即使异步通知失败,业务应用也能及时更新订单状态。

b,防止订单重复提交,可以通过业务数据唯一性生成字段,比如根据订单核心信息(单号,金额,日期,买方等核心信息)生成唯一哈希值,然后根据该唯一值创建重复提交判断,一般会创建一个分布式锁注解,通过配置索的key,在Redis中检查是否存在相同的标识,若存在则不允许重复提交,不存在则生成新的标识并设置过期时间,然后创建订单。
示例,分布式锁注解:

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedLock {
    String key() default "";

    long timeout() default 5L;
}

AOP实现具体的锁的过程

@Aspect
@Component
@EnableAspectJAutoProxy(
    exposeProxy = true
)
public class DistributedLockAspect {
    private static final Logger log = LoggerFactory.getLogger(DistributedLockAspect.class);
    @Autowired
    private RedisUtil redisUtil;
    private static RedisUtil REDIS_UTIL;

    @PostConstruct
    public void init() {
        REDIS_UTIL = this.redisUtil;
    }

    public DistributedLockAspect() {
    }

    @Around("@annotation(com.atshuo.annotation.DistributedLock)")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        HttpServletRequest request = ((ServletRequestAttributes)Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
        String ip = WebUtils.getIP(request);
        MethodSignature signature = (MethodSignature)point.getSignature();
        Method method = signature.getMethod();
        String className = method.getDeclaringClass().getName();
        String methodName = method.getName();
        String methodKey = String.format("%s#%s", className, methodName);
        int ipCode = MathUtils.abs(ip.hashCode());
        int methodCode = MathUtils.abs(methodKey.hashCode());
        String keyFmt = "%s_%d";
        String key = String.format(keyFmt, ipCode, methodCode);
        DistributedLock distributedLock = (DistributedLock)method.getAnnotation(DistributedLock.class);
        if (!sameUrlSubmit.isIpKey()) {
            key = String.format(keyFmt, "", methodCode);
        }

        if (StringUtils.isNotEmpty(distributedLock.key())) {
            List<String> paramNameList = Arrays.asList(signature.getParameterNames());
            List<Object> paramList = Arrays.asList(point.getArgs());
            ExpressionParser parser = new SpelExpressionParser();
            EvaluationContext ctx = new StandardEvaluationContext();

            int keyCode;
            for(keyCode = 0; keyCode < paramNameList.size(); ++keyCode) {
                ctx.setVariable((String)paramNameList.get(keyCode), paramList.get(keyCode));
            }

            keyCode = MathUtils.abs(parser.parseExpression(sameUrlSubmit.key()).getValue(ctx).toString().hashCode());
            key = key + "_" + keyCode;
        }

        long timeout = distributedLock.timeout();
        if (timeout < 0L) {
            timeout = CommonConstant.TIME_TO_SUBMIT;
        }

        boolean result = false;
        String cacheKey = "DistributedLock Key:" + key;

        try {
            result = REDIS_UTIL.setNX(cacheKey, UUID.randomUUID().toString(), timeout * 1000L);
        } catch (Exception var19) {
            if (Objects.isNull(REDIS_UTIL.get(cacheKey))) {
                REDIS_UTIL.set(cacheKey, UUID.randomUUID().toString());
                REDIS_UTIL.expire(cacheKey, timeout);
                return true;
            }

            return false;
        }

        if (!result) {
            throw new RuntimeException("请勿重复提交");
        } else {
            return point.proceed();
        }
    }
}

应用示例:在需要进行防重复提交的业务方法,增加注解,指定key和超时时间,截图如下
在这里插入图片描述

微信支付最佳实践

微信支付作为业界的佼佼者,提出了一些建议来优化支付系统:

1,重复提交锁:通过订单在提交支付时,先生成预支付单号,然后再发起微信支付,从而在外部实现防重复。而业务系统生成预支付单也进行防重复,即可保证整个订单支付环节的防重复支付。

2,合理设置超时时间: 根据支付业务的特点和实际情况,合理设置支付超时时间,确保用户支付体验和系统的高效运行。

3,合理使用异步通知: 合理设置异步通知的机制,保证系统可靠性和数据的一致性。

4,灵活配置支付方式: 根据不同支付方式的特点,进行灵活配置,确保系统的稳定性和安全性。
在这里插入图片描述

总结
通过上述措施,大型在线支付系统能够有效地防止订单重复支付,保障了用户的支付安全和交易顺利进行。从支付流水状态的控制、超时处理的设定,到异步通知和接口的幂等性保障,再到业务应用的主动查询和订单重复提交的防范,每个环节都是为了构建一个安全、高效的支付生态系统。

对于普通用户而言,这些繁琐的步骤都在幕后默默保护着我们的每一笔交易,确保我们的支付安全和便利。因此,在享受数字支付带来的便捷时,也可以更加放心地相信这些大厂背后的支付系统,它们正在用技术的力量为我们的支付保驾护航。

最后

从时代发展的角度看,网络安全的知识是学不完的,而且以后要学的会更多,同学们要摆正心态,既然选择入门网络安全,就不能仅仅只是入门程度而已,能力越强机会才越多。

因为入门学习阶段知识点比较杂,所以我讲得比较笼统,大家如果有不懂的地方可以找我咨询,我保证知无不言言无不尽,需要相关资料也可以找我要,我的网盘里一大堆资料都在吃灰呢。

干货主要有:

①1000+CTF历届题库(主流和经典的应该都有了)

②CTF技术文档(最全中文版)

③项目源码(四五十个有趣且经典的练手项目及源码)

④ CTF大赛、web安全、渗透测试方面的视频(适合小白学习)

⑤ 网络安全学习路线图(告别不入流的学习)

⑥ CTF/渗透测试工具镜像文件大全

⑦ 2023密码学/隐身术/PWN技术手册大全

如果你对网络安全入门感兴趣,那么你需要的话可以点击这里👉网络安全重磅福利:入门&进阶全套282G学习资源包免费分享!

扫码领取

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值