• 首页

  • 归档

  • 分类

  • 标签

  • 喵星人

  • 心情

  • 关于
W e A r e F a m i l y ! m i a o ~
W e A r e F a m i l y ! m i a o ~

柴子

青春流逝,记录往昔

12月
30
后端

SpringBoot防重复提交与限流的优雅解决

发表于 2021-12-30 • 字数统计 4182 • 被 546 人看爆

问题

  • 表单防重复提交一直是一个必须解决的接口安全问题
  • 接口限流也是时长要处理的问题,如短信验证码获取接口

如何优雅的解决呢

技术点

  • 自定义注解
  • 反射
  • AOP
  • Redis lua脚本

利用这四个技术的结合,既优雅又简化开发可方便用在任何接口

自定义注解

package com.cz.annotation;


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 限流
 *
 * @author chaixuhong
 * @date 2021年12月30日
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Limit {
    //功能描述,不参与逻辑
    String description() default "默认防重复请求";

    // 时间的,单位秒
    String period() default "5";

    // 限制访问次数
    String count() default "1";

    // 限制类型
    LimitType limitType() default LimitType.CUSTOMER;


    enum LimitType {
        CUSTOMER,
        IP
    }

}

  • 默认放重复提交功能,接口5秒内只能请求一次,当然可自行根据业务调整

AOP + 反射

/*
 *  Copyright 2019-2020 Zheng Jie
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.cz.aspect;

import com.cz.annotation.Limit;
import com.cz.exception.BusinessException;
import com.cz.utils.RequestParseUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;

/**
 * @author chaixuhong 
 * @date 2021年12月30日
 */
@Aspect
@Component
@Slf4j
public class LimitAspect {

    public static final String REDIS_PREFIX = "limit:";

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Pointcut("@annotation(com.cz.annotation.Limit)")
    public void pointcut() {
    }

    @Around("pointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        HttpServletRequest request = RequestParseUtil.getHttpServletRequest();
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method signatureMethod = signature.getMethod();
        Limit limit = signatureMethod.getAnnotation(Limit.class);
        Limit.LimitType limitType = limit.limitType();
        StringBuffer key = new StringBuffer(REDIS_PREFIX).append(joinPoint.getTarget().getClass().getCanonicalName()).append(".").append(signatureMethod.getName());
        if (limitType == Limit.LimitType.IP) {
            key.append(":").append(RequestParseUtil.getIp(request));
        }
        List<String> keys = Arrays.asList(key.toString());
        DefaultRedisScript<Number> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("script/Limit.lua")));
        redisScript.setResultType(Number.class);
        Number count = stringRedisTemplate.execute(redisScript, keys, limit.count(), limit.period());
        if (null != count && count.intValue() <= Integer.parseInt(limit.count())) {
            log.info("第{}次访问key为 {}的接口,功能描述:{}", count, keys, limit.description());
            return joinPoint.proceed();
        } else {
            throw new BusinessException("访问次数受限制");
        }
    }

}

  • 通过Redis lua脚本保证操作的原子性,多线程结果的一致性

lua脚本

local c

c = redis.call('get',KEYS[1])

if c and tonumber(c) > tonumber(ARGV[1]) then
    return tonumber(c);
end
c = redis.call('incr',KEYS[1])

if tonumber(c) == 1 then
    redis.call('expire',KEYS[1],ARGV[2])
end

return c;

如何使用

  • 只需要在接口上增加@Limit注解就可以了
  • 示例
    //@Limit 什么参数不写默认防重复提交功能	
    @Limit(description = "IP限流", period = "60", count = "10", limitType = Limit.LimitType.IP)
    @GetMapping(value = "/child")
    public ResultVO<DeptVO> child(@RequestParam(required = false) Integer pid, @RequestParam(required = false) String name) {
        return ResponseUtil.success(sysDeptService.child(pid, name));
    }
分享到:
mybatis-plus自定义统一填充字段内容
elementui tree 懒加载只触发一次问题
  • 文章目录
  • 站点概览
柴子

内蒙 柴子

what do U want?

Github QQ Email RSS
最喜欢的电影
最喜欢的游戏
最喜欢的音乐
最喜欢的图书
最喜欢的动漫
夏洛特的烦恼
英雄联盟
痴心绝对
数据库从入门到删库跑路
斗破苍穹
看爆 Top5
  • 微信getUserProfile兼容性调整以及uniapp写法 2,041次看爆
  • gateway转发微服务请求丢失header参数 1,016次看爆
  • mybatis-plus代码生成器 950次看爆
  • SpringBoot防重复提交与限流的优雅解决 547次看爆
  • Spring Boot Security从入门到进阶到高级 544次看爆
转载注明出处

站点已萌萌哒运行 00 天 00 小时 00 分 00 秒(●'◡'●)ノ♥

Copyright © 2022 柴子 京ICP备17035556号-1

由 Halo 强力驱动 · Theme by Sagiri · 站点地图