问题:
在日常开发中,一些重要的对外接口,需要加上访问频率限制,以免造成资��损失。
如登录接口,当用户使用手机号+验证码登录时,一般我们会生成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访问此接口进行验证。