接口防刷!利用Redisson快速实现自定义限流注解

数据库 Redis
利用Redis的有序集合即Sorted Set数据结构,构造一个令牌桶来实施限流。而Redisson已经帮我们封装成了RRateLimiter,通过Redisson,即可快速实现我们的目标。

问题:

在日常开发中,一些重要的对外接口,需要添加访问频率限制,以免造成资产损失。

如登录接口,当用户使用手机号+验证码登录时,一般我们会生成6位数的随机验证码,并将验证码有效期设置为1-3分钟,如果对登录接口不加以限制,理论上,通过技术手段,快速重试100000次,即可将验证码穷举出来。

解决思路:

对登录接口加上限流操作,如限制一分钟内最多登录5次,登录次数过多,就返回失败提示,或者将账号锁定一段时间。

实现手段:

利用redis的有序集合即Sorted Set数据结构,构造一个令牌桶来实施限流。而redisson已经帮我们封装成了RRateLimiter,通过redisson,即可快速实现我们的目标。

1. 定义一个限流注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface GlobalRateLimiter {

    String key();

    long rate();

    long rateInterval() default 1L;

    RateIntervalUnit rateIntervalUnit() default RateIntervalUnit.SECONDS;

}

2. 利用aop进行切面

@Aspect
@Component
@Slf4j
public class GlobalRateLimiterAspect {

    @Resource
    private Redisson redisson;
    @Value("${spring.application.name}")
    private String applicationName;
    private final DefaultParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer();

    @Pointcut(value = "@annotation(com.zj.demoshow.annotion.GlobalRateLimiter)")
    public void cut() {
    }

    @Around(value = "cut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        String className = method.getDeclaringClass().getName();
        String methodName = method.getName();
        GlobalRateLimiter globalRateLimiter = method.getDeclaredAnnotation(GlobalRateLimiter.class);
        Object[] params = joinPoint.getArgs();
        long rate = globalRateLimiter.rate();
        String key = globalRateLimiter.key();
        long rateInterval = globalRateLimiter.rateInterval();
        RateIntervalUnit rateIntervalUnit = globalRateLimiter.rateIntervalUnit();
        if (key.contains("#")) {
            ExpressionParser parser = new SpelExpressionParser();
            StandardEvaluationContext ctx = new StandardEvaluationContext();
            String[] parameterNames = discoverer.getParameterNames(method);
            if (parameterNames != null) {
                for (int i = 0; i < parameterNames.length; i++) {
                    ctx.setVariable(parameterNames[i], params[i]);
                }
            }
            Expression expression = parser.parseExpression(key);
            Object value = expression.getValue(ctx);
            if (value == null) {
                throw new RuntimeException("key无效");
            }
            key = value.toString();
        }
        key = applicationName + "_" + className + "_" + methodName + "_" + key;
        log.info("设置限流锁key={}", key);
        RRateLimiter rateLimiter = this.redisson.getRateLimiter(key);
        if (!rateLimiter.isExists()) {
            log.info("设置流量,rate={},rateInterval={},rateIntervalUnit={}", rate, rateInterval, rateIntervalUnit);
            rateLimiter.trySetRate(RateType.OVERALL, rate, rateInterval, rateIntervalUnit);
            //设置一个过期时间,避免key一直存在浪费内存,这里设置为延长5分钟
            long millis = rateIntervalUnit.toMillis(rateInterval);
            this.redisson.getBucket(key).expire(Long.sum(5 * 1000 * 60, millis), TimeUnit.MILLISECONDS);
        }
        boolean acquire = rateLimiter.tryAcquire(1);
        if (!acquire) {
            //这里直接抛出了异常  也可以抛出自定义异常,通过全局异常处理器拦截进行一些其他逻辑的处理
            throw new RuntimeException("请求频率过高,此操作已被限制");
        }
        return joinPoint.proceed();
    }
}

ok,通过以上两步,即可完成我们的限流注解了,下面通过一个接口验证下效果。

新建一个controller,写一个模拟登录的方法。

@RestController
@RequestMapping(value = "/user")
public class UserController {


    @PostMapping(value = "/testForLogin")
    //以account为锁的key,限制每分钟最多登录5次
    @GlobalRateLimiter(key = "#params.account", rate = 5, rateInterval = 60)
    R<Object> testForLogin(@RequestBody @Validated LoginParams params) {
        //登录逻辑
        return R.success("登录成功");
    }
}

启动服务,通过postman访问此接口进行验证。

可以看到,在第6次访问接口的时候,抛出了请求限制的异常。

注意点:

设置key的时候,一定要注意唯一性,比如登录接口,可以将登录账号作为唯一性,查询某个人的订单记录时,将用户id作为唯一性,要避免无意义的key,以免误造成全局接口的限流。

设置rateLimiter的rate时,RateType有两种模式:全局 or 客户端,可以根据需求自主设置,一般都使用全局。

责任编辑:姜华 来源: 今日头条
相关推荐

2024-04-01 08:11:20

2023-10-24 13:48:50

自定义注解举值验证

2022-12-13 09:19:06

高并发SpringBoot

2023-03-03 09:11:12

高并发SpringBoot

2022-11-01 11:15:56

接口策略模式

2015-02-12 15:33:43

微信SDK

2023-09-04 08:12:16

分布式锁Springboot

2017-08-03 17:00:54

Springmvc任务执行器

2023-10-09 07:37:01

2015-02-12 15:38:26

微信SDK

2021-12-30 12:30:01

Java注解编译器

2022-02-17 07:10:39

Nest自定义注解

2023-10-11 07:57:23

springboot微服务

2009-08-21 15:38:45

ControllerF

2009-09-07 22:00:15

LINQ自定义

2009-11-09 16:06:53

WCF自定义集合

2011-04-06 10:03:08

Cacti远程监控

2011-03-31 16:03:20

cacti性能计数器

2022-05-18 07:44:13

自定义菜单前端

2020-11-25 11:20:44

Spring注解Java
点赞
收藏

51CTO技术栈公众号