使用spring拦截器做频率限制


1. 注解类 Frequency

@Target ({ElementType.TYPE, ElementType.METHOD})    
@Retention (RetentionPolicy.RUNTIME)    
@Documented     
@Component    
public @interface Frequency {

	String name() default "all";
	int time()  default 0;
	int limit()  default 0;
}

2. 拦截对象封装 FrequencyStruct

public class FrequencyStruct {

	String uniqueKey;
	long start;
	long end;
	int time;
	int limit;
	List<Long> accessPoints = new ArrayList<Long>();
	
	public void reset(long timeMillis) {
		
		start = end = timeMillis;
		accessPoints.clear();
		accessPoints.add(timeMillis);
	}

	@Override
	public String toString() {
		return "FrequencyStruct [uniqueKey=" + uniqueKey + ", start=" + start
				+ ", end=" + end + ", time=" + time + ", limit=" + limit
				+ ", accessPoints=" + accessPoints + "]";
	}
}


3. FrequencyHandlerInterceptor会拦截所有带Frequency注解的类或方法

如果是有负载情况下,会取x-forwarded-for头里的ip地址,经过负载的请求必须带x-forwarded-for头,记录用户ip。

频率限制使用本地内存做数据基站,初始化时会开辟MAX_BASE_STATION_SIZE长度的HashMap,MAX_BASE_STATION_SIZE得默认值是100000。

public class FrequencyHandlerInterceptor extends HandlerInterceptorAdapter {
	
	private Logger logger = LoggerFactory.getLogger(FrequencyHandlerInterceptor.class); 
	private static final int MAX_BASE_STATION_SIZE = 100000;
	private static Map<String, FrequencyStruct> BASE_STATION = new HashMap<String, FrequencyStruct>(MAX_BASE_STATION_SIZE);
	private static final float SCALE = 0.75F;
	private static final int MAX_CLEANUP_COUNT = 3;
	private static final int CLEANUP_INTERVAL = 1000;
	private Object syncRoot = new Object();
	private int cleanupCount = 0;
	
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
		
		Frequency methodFrequency = ((HandlerMethod) handler).getMethodAnnotation(Frequency.class);
		Frequency classFrequency = ((HandlerMethod) handler).getBean().getClass().getAnnotation(Frequency.class);
		
		boolean going = true;
		if(classFrequency != null) {
			going = handleFrequency(request, response, classFrequency);
		}
		
		if(going && methodFrequency != null) {
			going = handleFrequency(request, response, methodFrequency);
		}
		return going;
	}

	private boolean handleFrequency(HttpServletRequest request, HttpServletResponse response, Frequency frequency) {
		
		boolean going = true;
		if(frequency == null) {
			return going;
		}
		
		String name = frequency.name();
		int limit = frequency.limit();
		int time = frequency.time();
		
		if(time == 0 || limit == 0) {
			going = false;
			response.setStatus(HttpServletResponse.SC_FORBIDDEN);
			return going;
		}
			
		long currentTimeMilles = System.currentTimeMillis() / 1000;
		
		String ip = getRemoteIp(request);
		String key = ip + "_" + name;
		FrequencyStruct frequencyStruct = BASE_STATION.get(key);
		
		if(frequencyStruct == null) {
			
			frequencyStruct = new FrequencyStruct();
			frequencyStruct.uniqueKey = name;
			frequencyStruct.start = frequencyStruct.end = currentTimeMilles;
			frequencyStruct.limit = limit;
			frequencyStruct.time = time;
			frequencyStruct.accessPoints.add(currentTimeMilles);

			synchronized (syncRoot) {
				BASE_STATION.put(key, frequencyStruct);
			}
			if(BASE_STATION.size() > MAX_BASE_STATION_SIZE * SCALE) {
				cleanup(currentTimeMilles);
			}
		} else {
			
			frequencyStruct.end = currentTimeMilles;
			frequencyStruct.accessPoints.add(currentTimeMilles);
		}
		
		//时间是否有效
		if(frequencyStruct.end - frequencyStruct.start >= time) {
			
			if(logger.isDebugEnabled()) {
				logger.debug("frequency struct be out of date, struct will be reset., struct: {}", frequencyStruct.toString());
			}
			frequencyStruct.reset(currentTimeMilles);
		} else {
			
			int count = frequencyStruct.accessPoints.size();
			if(count > limit) {
				if(logger.isDebugEnabled()) {
					logger.debug("key: {} too frequency. count: {}, limit: {}.", key, count, limit);
				}
				going = false;
				response.setStatus(HttpServletResponse.SC_FORBIDDEN);
			}
		}
		return going;
	}

	private void cleanup(long currentTimeMilles) {
		
		synchronized (syncRoot) {
			
			Iterator<String> it = BASE_STATION.keySet().iterator();
			while(it.hasNext()) {
				
				String key = it.next();
				FrequencyStruct struct = BASE_STATION.get(key);
				if((currentTimeMilles - struct.end) > struct.time) {
					it.remove();
				}
			}
			
			if((MAX_BASE_STATION_SIZE - BASE_STATION.size()) > CLEANUP_INTERVAL) {
				cleanupCount = 0;
			} else {
				cleanupCount++;
			}
			
			if(cleanupCount > MAX_CLEANUP_COUNT ) {
				randomCleanup(MAX_CLEANUP_COUNT);
			}
		}
	}

	/**
	 * 随机淘汰count个key
	 * 
	 * @param maxCleanupCount
	 */
	private void randomCleanup(int count) {
		//防止调用错误
		if(BASE_STATION.size() < MAX_BASE_STATION_SIZE * SCALE) {
			return;
		}
		
		Iterator<String> it = BASE_STATION.keySet().iterator();
		Random random = new Random();
		int tempCount = 0;
		
		while(it.hasNext()) {
			if(random.nextBoolean()) {
				it.remove();
				tempCount++;
				if(tempCount >= count) {
					break;
				}
			}
		}
	}
	
	private String getRemoteIp(HttpServletRequest request) {

		String ip = request.getHeader("x-forwarded-for");
		if(StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getHeader("Proxy-Client-IP");
		}

		if(StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getHeader("WL-Proxy-Client-IP");
		}

		if(StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getRemoteAddr();
		}

		return ip;

	}
}

4. 配置使用

<!-- 拦截器配置 -->
	<mvc:interceptors>  
	    <!-- 国际化操作拦截器 如果采用基于(请求/Session/Cookie)则必需配置 --> 
	    <bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor" />  
	    <!-- 如果不定义 mvc:mapping path 将拦截所有的URL请求 -->
	    <bean class="xxx.annotation.FrequencyHandlerInterceptor"></bean>
	</mvc:interceptors>

5. 可以使用在Controller或单独某个方法上。最好给每个name都定义单独的name,默认all的范围太广,使用方法如下:

@Controller
@RequestMapping("/demo")
@Frequency(name="demo", limit=3, time=1)
public class DemoController {

    @RequestMapping(value = {"index"})

     @Frequency(name="method", limit=3, time=1)

     public void method()

}




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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值