当我们需要对后端的某些接口进行限流(其实防止一些请求在一定时间内进行多次访问,比如防止用户1秒内多次进行评论、防止多次重复登录等操作,这时我们就需要对该接口进行限流)
当然限流操作还有一些场景:
- 秒杀活动,有人使用软件恶意刷单抢货,需要限流防止机器参与活动
- 某api被各式各样系统广泛调用,严重消耗网络、内存等资源,需要合理限流
- 淘宝获取ip所在城市接口、微信公众号识别微信用户等开发接口,免费提供给用户时需要限流,更具有实时性和准确性的接口需要付费。
总的就是说防止同一用户对单个接口进行重复调用,这里我们就需要使用到@AccessLimit进行流量控制。这个注解需要我们自己手动去定义,并搭配springboot的拦截器使用。
原理分析
拦截器拦截一个请求,看看这个请求所访问的接口方法上是否存在我们的注解@AccessLimit,如果存在,则取出注解里的相关信息(例如在多少时间内允许访问接口多少次,超过限制会提示出怎么样的内容),再从这个request请求中取出ip地址,访问的路径等组合为key作为一个用户的唯一标识。在redis中查找是否有这个key,没有则之后以唯一标识:访问次数
map的形式存放到redis中,并设置过期时间;如果存在,则取出这个key对应的value值比较是否超出我们的预定值,超出则给出提示信息,并return false拦截该请求(该请求结束),如果并没有超出限制,则给这个key+1后return true对该request进行放行
我们先来自定以这个注解
AccessLimit
@Target(ElementType.METHOD) //这个注解应该用到方法上
@Retention(RetentionPolicy.RUNTIME) //运行时注解
public @interface AccessLimit {
/**
* 限制周期(秒)
*/
int seconds();
/**
* 规定周期内限制次数
*/
int maxCount();
/**
* 触发限制时的消息提示
*/
String msg() default "操作频率过高";
}
定义拦截器AccessLimitInterceptor
@Component
public class AccessLimitInterceptor extends HandlerInterceptorAdapter {
//本拦截器需要使用redis缓存来进行计数
@Autowired
RedisService redisService;
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//看看这个handler是不是一个方法,不是的话直接放行(AccessLimit只会出现在方法上)
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
AccessLimit accessLimit = handlerMethod.getMethodAnnotation(AccessLimit.class);
//方法上没有访问控制的注解,直接通过
if (accessLimit == null) {
return true;
}
//获取时间限制
int seconds = accessLimit.seconds();
//获取数量限制
int maxCount = accessLimit.maxCount();
//获取ip地址
String ip = IpAddressUtils.getIpAddress(request);
//获取请求方法
String method = request.getMethod();
//获取请求路径
String requestURI = request.getRequestURI();
//组合成唯一的k
String redisKey = ip + ":" + method + ":" + requestURI;
Integer count = redisService.getObjectByValue(redisKey, Integer.class);
if (count == null) {
//在规定周期内第一次访问,存入redis
redisService.incrementByKey(redisKey, 1);
redisService.expire(redisKey, seconds);
} else {
if (count >= maxCount) {
//超出访问限制次数
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
Result result = Result.create(403, accessLimit.msg());
//让response包含我们的result对象
out.write(JacksonUtils.writeValueAsString(result));
out.flush();
out.close();
return false;
} else {
//没超出访问限制次数
redisService.incrementByKey(redisKey, 1);
}
}
}
return true;
}
}
其中有一个方法会通过request获得ip地址,这里将代码贴出来:
/**
* 在Nginx等代理之后获取用户真实IP地址
*
* @param request
* @return
*/
public static String getIpAddress(HttpServletRequest request) {
String ip = request.getHeader("X-Real-IP");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("x-forwarded-for");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) {
//根据网卡取本机配置的IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
log.error("getIpAddress exception:", e);
}
ip = inet.getHostAddress();
}
}
return StringUtils.substringBefore(ip, ",");
}
定义了拦截器,我们还需要将拦截器注册到springboot
注册拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(accessLimitInterceptor); //默认是为所有的接口都设置拦截器
}
}
之后我们就可以在Controller层使用注解了
@GetMapping("/test")
@AccessLimit(seconds=5, maxCount=5 , msg="超过访问次数")
public Result test(){
return new Result();
}
如果我们在规定时间内访问统一接口,就会有所提示并拦截掉request。