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()
}