@AccessLimit接口限流

当我们需要对后端的某些接口进行限流(其实防止一些请求在一定时间内进行多次访问,比如防止用户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。

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值