@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
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
可以通过使用拦截器或者过滤器实现对指定接口的访问次数限制。 以下是使用拦截器实现限制访问次数的示例代码: 1. 创建自定义注解 `@AccessLimit`,用于标记需要限制访问次数的接口。 ```java @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AccessLimit { // 默认访问次数限制为5次 int limit() default 5; // 时间段,单位为秒,默认为60秒 int seconds() default 60; } ``` 2. 创建拦截器 `AccessLimitInterceptor`,用于实现限制访问次数的逻辑。 ```java @Component public class AccessLimitInterceptor implements HandlerInterceptor { @Autowired private RedisTemplate<String, Object> redisTemplate; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 判断是否标注了@AccessLimit注解 if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; if (!handlerMethod.hasMethodAnnotation(AccessLimit.class)) { return true; } // 获取@AccessLimit注解 AccessLimit accessLimit = handlerMethod.getMethodAnnotation(AccessLimit.class); // 获取接口访问限制次数和时间段 int limit = accessLimit.limit(); int seconds = accessLimit.seconds(); // 获取请求的IP地址和接口地址 String ipAddr = getIpAddress(request); String requestUrl = request.getRequestURI(); // 设置Redis中的Key String redisKey = String.format("%s_%s", ipAddr, requestUrl); // 判断Redis中是否存在Key ValueOperations<String, Object> valueOps = redisTemplate.opsForValue(); if (!redisTemplate.hasKey(redisKey)) { // 第一次访问,设置初始值 valueOps.set(redisKey, 1, seconds, TimeUnit.SECONDS); } else { // 已经访问过,进行访问次数限制判断 int count = (int) valueOps.get(redisKey); if (count >= limit) { // 超出访问次数限制,返回错误信息 response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("超出访问次数限制"); return false; } else { // 访问次数加1 valueOps.increment(redisKey, 1); } } } return true; } /** * 获取请求的IP地址 */ private String getIpAddress(HttpServletRequest request) { String ipAddr = request.getHeader("x-forwarded-for"); if (StringUtils.isBlank(ipAddr) || "unknown".equalsIgnoreCase(ipAddr)) { ipAddr = request.getHeader("Proxy-Client-IP"); } if (StringUtils.isBlank(ipAddr) || "unknown".equalsIgnoreCase(ipAddr)) { ipAddr = request.getHeader("WL-Proxy-Client-IP"); } if (StringUtils.isBlank(ipAddr) || "unknown".equalsIgnoreCase(ipAddr)) { ipAddr = request.getRemoteAddr(); } return ipAddr; } } ``` 3. 在需要限制访问次数的接口上添加 `@AccessLimit` 注解。 ```java @RestController public class DemoController { @GetMapping("/demo") @AccessLimit public String demo() { return "Hello World!"; } } ``` 这样,每个IP地址在60秒内最多只能访问 `/demo` 接口5次。 注意:以上代码仅供参考,实际应用中还需要考虑并发访问、线程安全等问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值