接口防刷!利用redisson快速實現自定義限流注解
問題:
在日常開發中,一些重要的對外接口,需要加上訪問頻率限制,以免造成資??損失。
如登錄接口,當用戶使用手機號+驗證碼登錄時,一般我們會生成6位數的隨機驗證碼,并將驗證碼有效期設置為1-3分鐘,如果對登錄接口不加以限制,理論上,通過技術手段,快速重試100000次,即可將驗證碼窮舉出來。
解決思路:對登錄接口加上限流操作,如限制一分鐘內最多登錄5次,登錄次數過多,就返回失敗提示,或者將賬號鎖定一段時間。
實現手段:利用redis的有序集合即Sorted Set數據結構,構造一個令牌桶來實施限流。而redisson已經幫我們封裝成了RRateLimiter,通過redisson,即可快速實現我們的目標。
-
定義一個限流注解
import org.redisson.api.RateIntervalUnit; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface GlobalRateLimiter { String key(); long rate(); long rateInterval() default 1L; RateIntervalUnit rateIntervalUnit() default RateIntervalUnit.SECONDS; } -
利用aop進行切面
import com.zj.demoshow.annotion.GlobalRateLimiter; import lombok.extern.slf4j.Slf4j; 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.redisson.Redisson; import org.redisson.api.RRateLimiter; import org.redisson.api.RateIntervalUnit; import org.redisson.api.RateType; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.lang.reflect.Method; import java.util.concurrent.TimeUnit; @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訪問此接口進行驗證。


浙公網安備 33010602011771號