接口防刷!利用Redisson快速實現(xiàn)自定義限流注解

問題:
在日常開發(fā)中,一些重要的對外接口,需要添加訪問頻率限制,以免造成資產(chǎn)損失。
如登錄接口,當用戶使用手機號+驗證碼登錄時,一般我們會生成6位數(shù)的隨機驗證碼,并將驗證碼有效期設(shè)置為1-3分鐘,如果對登錄接口不加以限制,理論上,通過技術(shù)手段,快速重試100000次,即可將驗證碼窮舉出來。
解決思路:
對登錄接口加上限流操作,如限制一分鐘內(nèi)最多登錄5次,登錄次數(shù)過多,就返回失敗提示,或者將賬號鎖定一段時間。
實現(xiàn)手段:
利用redis的有序集合即Sorted Set數(shù)據(jù)結(jié)構(gòu),構(gòu)造一個令牌桶來實施限流。而redisson已經(jīng)幫我們封裝成了RRateLimiter,通過redisson,即可快速實現(xiàn)我們的目標。
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("設(shè)置限流鎖key={}", key);
        RRateLimiter rateLimiter = this.redisson.getRateLimiter(key);
        if (!rateLimiter.isExists()) {
            log.info("設(shè)置流量,rate={},rateInterval={},rateIntervalUnit={}", rate, rateInterval, rateIntervalUnit);
            rateLimiter.trySetRate(RateType.OVERALL, rate, rateInterval, rateIntervalUnit);
            //設(shè)置一個過期時間,避免key一直存在浪費內(nèi)存,這里設(shè)置為延長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("登錄成功");
    }
}啟動服務(wù),通過postman訪問此接口進行驗證。

可以看到,在第6次訪問接口的時候,拋出了請求限制的異常。
注意點:
設(shè)置key的時候,一定要注意唯一性,比如登錄接口,可以將登錄賬號作為唯一性,查詢某個人的訂單記錄時,將用戶id作為唯一性,要避免無意義的key,以免誤造成全局接口的限流。
設(shè)置rateLimiter的rate時,RateType有兩種模式:全局 or 客戶端,可以根據(jù)需求自主設(shè)置,一般都使用全局。















 
 
 















 
 
 
 