基于自定义注解的多场景防重提交设计

1.项目开发中可能碰到情况

 1.1 前端下单按钮重复点击导致订单创建多次
 1.2 网速等原因造成页面卡顿,用户重复刷新提交请求
 1.3 黑客或恶意用户使用postman等http工具重复恶意提交表单

2.防重提交的几种方式

 2.1 前端JS控制点击次数,屏蔽点击按钮无法点击
      劣势:前端可能被绕过
 2.2 数据库或者其他存储增加唯一索引约束
      劣势:需要满足业务需求的唯一约束,如手机号唯一,不满足全部业务
  2.3 服务端token令牌方式
     **下单前先获取令牌-存储red`在这里插入代码片`is 下单时一并把token提交并检验和删除-lua脚本(绝大部分都采用此种方式)   
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";

3.自定义注解实现防重提交

下面为大家介绍一种使用自定义注解的方式实现防重提交,代码会看起来更优雅。
3.1 定义自定义注解类(RepeatSubmit)

import java.lang.annotation.*;
/**
 * 自定义防重提交
 */
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatSubmit {


    /**
     * 防重提交,支持两种,一个是方法参数,一个是令牌
     */
    enum Type { PARAM, TOKEN }

    /**
     * 默认防重提交,是方法参数
     * @return
     */
    Type limitType() default Type.PARAM;


    /**
     * 加锁过期时间,默认是5秒
     * @return
     */
    long lockTime() default 5;

}

3.2 定义切面类(RepeatSubmitAspect)
/**
 * 定义一个切面类
 **/

@Aspect
@Component
@Slf4j
public class RepeatSubmitAspect {


    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private RedissonClient redissonClient;
    /**
     * 定义 @Pointcut注解表达式,
     * 方式一:@annotation:当执行的方法上拥有指定的注解时生效(我们采用这)
     * 方式二:execution:一般用于指定方法的执行
     */
    @Pointcut("@annotation(repeatSubmit)")
    public void pointCutNoRepeatSubmit(RepeatSubmit repeatSubmit) {

    }


    /**
     * 环绕通知, 围绕着方法执行
     *
     * @param joinPoint
     * @param
     * @return
     * @throws Throwable
     * @Around 可以用来在调用一个具体方法前和调用后来完成一些具体的任务。
     * <p>
     * 方式一:单用 @Around可以
     * 方式二:用@Pointcut和@Around联合注解也可以(我们采用这个)
     * <p>
     * <p>
     * 两种方式
     * 方式一:加锁 固定时间内不能重复提交
     * <p>
     * 方式二:先请求获取token,这边再删除token,删除成功则是第一次提交
     */
    @Around("pointCutNoRepeatSubmit(repeatSubmit)")
    public Object around(ProceedingJoinPoint joinPoint, RepeatSubmit repeatSubmit) throws Throwable {

        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();

        long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();

        //用于记录成功或者失败
        boolean res = false;


        //防重提交类型
        String type = repeatSubmit.limitType().name();
        if(type.equalsIgnoreCase(RepeatSubmit.Type.PARAM.name())){
            //方式一,参数形式防重提交

            long lockTime = repeatSubmit.lockTime();

            String ipAddr = CommonUtil.getIpAddr(request);

            MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();

            Method method = methodSignature.getMethod();

            String className = method.getDeclaringClass().getName();

            String key = "order-server:repeat_submit:"+CommonUtil.MD5(String.format("%s-%s-%s-%s",ipAddr,className,method,accountNo));

            //加锁
            RLock lock = redissonClient.getLock(key);
            // 尝试加锁,最多等待0秒,上锁以后5秒自动解锁 [lockTime默认为5s, 可以自定义]
            res = lock.tryLock(0,lockTime,TimeUnit.SECONDS);
        }else {
            //方式二,令牌形式防重提交
            String requestToken = request.getHeader("request-token");
            if(StringUtils.isBlank(requestToken)){
                throw new BizException(BizCodeEnum.ORDER_CONFIRM_TOKEN_EQUAL_FAIL);
            }

            String key = String.format(RedisKey.SUBMIT_ORDER_TOKEN_KEY,accountNo,requestToken);

            /**
             * 提交表单的token key
             * 方式一:不用lua脚本获取再判断,之前是因为 key组成是 order:submit:accountNo, value是对应的token,所以需要先获取值,再判断
             * 方式二:可以直接key是 order:submit:accountNo:token,然后直接删除成功则完成
             */
            res = redisTemplate.delete(key);

        }
        if(!res){
            log.error("请求重复提交");
            return null;
        }


        log.info("环绕通知执行前");

        Object obj = joinPoint.proceed();

        log.info("环绕通知执行后");

        return obj;

    }


}

3.3 方法上加入注解( @RepeatSubmit(limitType = RepeatSubmit.Type.TOKEN))

    /**
     * 下单接口
     * @param orderRequest
     * @param response
     */
    @PostMapping("confirm")
    @RepeatSubmit(limitType = RepeatSubmit.Type.TOKEN)
    public void confirmOrder(@RequestBody ConfirmOrderRequest orderRequest, HttpServletResponse response) {

        JsonData jsonData = productOrderService.confirmOrder(orderRequest);

        if (jsonData.getCode() == 0) {

            //端类型
            String client = orderRequest.getClientType();
            //支付类型
            String payType = orderRequest.getPayType();

            //如果是支付宝支付,跳转网页,sdk除外
            if (payType.equalsIgnoreCase(ProductOrderPayTypeEnum.ALI_PAY.name())) {

                if (client.equalsIgnoreCase(ClientTypeEnum.PC.name())) {

                    CommonUtil.sendHtmlMessage(response, jsonData);

                } else if (client.equalsIgnoreCase(ClientTypeEnum.APP.name())) {

                } else if (client.equalsIgnoreCase(ClientTypeEnum.H5.name())) {

                }

            } else if (payType.equalsIgnoreCase(ProductOrderPayTypeEnum.WECHAT_APY.name())) {
                //微信支付
                CommonUtil.sendJsonMessage(response, jsonData);
            }

        } else {
            log.error("创建订单失败{}", jsonData.toString());
            CommonUtil.sendJsonMessage(response, jsonData);
        }

    }
  接口需要做防重的,打上这个注解即可,默认使用参数形式。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值