根据用户ip地址限制其访问在一定时间内访问某些接口的次数,运营人员可以根据接口的描述设置该接口在指定的时间内可以访问的次数,超出访问次数提醒请求次数超限,通过在需要限定的接口上加注解来标识哪些方法需要被限制,且这些需要被限制的方法运营可以通过页面设置每一个方法在设定的时间内可被同一个用户(ip地址),访问的次数。
1:需要限定的接口详情表(运营可根据接口描述来自定义改接口在“单位时间”内被限定的“次数”)
CREATE TABLE `refresh_limit_bean` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`description` varchar(100) Not NULL COMMENT '接口描述',
`method_path` varchar(400) not NULL COMMENT '接口地址',
`count` int Not NULL COMMENT '次数',
`time` int not NULL COMMENT '单位时间',
PRIMARY KEY (`id`),
UNIQUE KEY `description` (`description`)
) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8 COMMENT='需要被限定的接口详情表';
2:接口详情表对应的实体类
package cn.arbexpress.clientmanager.aop;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;
/**
1. DESCRIPTION:限制访问实体类
@author liu.qq
@create 2019-09-12 14:59
**/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(name = "refresh_limit_bean")
public class RefreshLimitBean implements Serializable {
private static final long serialVersionUID = -5065600689378738881L;
@Id
@Column(name = "id")
@GeneratedValue(generator = "JDBC")
private Integer id;//案件id,主键
@Column(name = "description")
private String description;
@Column(name = "method_path")
private String methodPath;
@Column(name = "count")
private Integer count;//配置单位时间内,某个接口可被访问次数的阈值
@Column(name = "time")
private Integer time;//设置时间值
//方法的描述是方法的唯一标识
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
RefreshLimitBean o1 = (RefreshLimitBean) o;
if(this.description.equals(o1.description)){
return true;
}
return false;
}
@Override
public int hashCode() {
int result = super.hashCode();
result = 31 * result + (id != null ? id.hashCode() : 0);
result = 31 * result + (description != null ? description.hashCode() : 0);
result = 31 * result + (methodPath != null ? methodPath.hashCode() : 0);
result = 31 * result + count;
result = 31 * result + time;
return result;
}
}
3: 定义注解标识所有需要被限定的目标接口对应的目标方法上
package cn.arbexpress.clientmanager.aop;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface LimitNoteAnnotation {
/*
* 接口信息描述
*/
String desc();
/**
* 允许访问的次数,默认值MAX_VALUE
*/
int count() default Integer.MAX_VALUE;
/**
* 时间段,单位为秒,默认值30秒钟
*/
long time() default 30;
}
4:监听容器刷新事件,注册新增的注解信息到DB
package cn.arbexpress.clientmanager.aop;
import cn.arbexpress.clientmanager.dao.mapper.RefreshLimitBeanMapper;
import org.reflections.Reflections;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* DESCRIPTION:监听容器刷新事件,注册新增的注解信息到DB
*
* @author liu.qq
* @create 2019-09-12 14:31
**/
@Component
public class RefreshLimitNoteAnnotationHandler implements ApplicationListener<ContextRefreshedEvent> {
@Autowired
private RefreshLimitBeanMapper refreshLimitBeanMapper;
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
//获取指定包下所有被Controller注解标识的Class集合
Reflections reflections = new Reflections("cn.arbexpress.clientmanager.controller");
Set<Class<?>> typesAnnotatedWith = reflections.getTypesAnnotatedWith(Controller.class);
//读取到所有标注了LimitNoteAnnotation注解的方法,放入Set集合
List<RefreshLimitBean> refreshLimitBeanList = new ArrayList<>(32);
for(Class clazz:typesAnnotatedWith){
Method[] methods = clazz.getMethods();
for(Method method:methods){
LimitNoteAnnotation annotation = method.getAnnotation(LimitNoteAnnotation.class);
if(annotation == null){
continue;
}
String desc = annotation.desc();
String methodDetailInfo = getMethodDetailInfo(method);
RefreshLimitBean refreshLimitBean = RefreshLimitBean.builder().description(desc).methodPath(methodDetailInfo).build();
refreshLimitBeanList.add(refreshLimitBean);
}
}
//获取之前已经设置过LimitNoteAnnotation注解的方法的相关接口
List<RefreshLimitBean> refreshLimitBeans = refreshLimitBeanMapper.selectAll();
//插入新增的注解方法到DB
for(RefreshLimitBean refreshLimitBean:refreshLimitBeanList){
if(!refreshLimitBeans.contains(refreshLimitBean)){
refreshLimitBeanMapper.insert(refreshLimitBean);
}
}
}
//获取方法的详细信息
private String getMethodDetailInfo(Method method){
Class<?> declaringClass = method.getDeclaringClass();
String className = declaringClass.getName();
String methodName = method.getName();
Class<?>[] parameterTypes = method.getParameterTypes();
StringBuilder sb = new StringBuilder();
sb.append(className).append(".").append(methodName);
for (int i = 0; i < parameterTypes.length; i++) {
if (i == 0) {
sb.append("(");
}
if (i == parameterTypes.length - 1) {
sb.append(parameterTypes[i].getName()).append(")");
} else {
sb.append(parameterTypes[i].getName()).append(",");
}
}
return sb.toString();
}
}
5:定义拦截器拦截每次请求判断当前ip访问的目标接口是否超出请求限制
package cn.arbexpress.clientmanager.aop;
import cn.arbexpress.clientmanager.dao.mapper.RefreshLimitBeanMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
/**
* DESCRIPTION:拦截每次请求判断当前ip访问的目标接口是否超出请求限制
*
* @author liu.qq
* @create 2019-09-12 15:55
**/
public class RequestLimitEnhanceInterceptor extends HandlerInterceptorAdapter {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private RefreshLimitBeanMapper refreshLimitBeanMapper;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
LimitNoteAnnotation limit = method.getAnnotation(LimitNoteAnnotation.class);
//目标方法没有贴LimitNoteAnnotation注解直接放行
if (limit == null) {
return true;
} else {
String remoteAddr = request.getRemoteAddr();//获取客户端ip地址
String key = "req_limt_".concat(getMethodNameWithParamterType(handlerMethod)).concat("_").concat(remoteAddr);
if (!checkUseRedis(limit, key,handlerMethod)) {
throw new RequestLimitException();
}
return true;
}
}
/*
使用缓存设置请求记录
*/
private boolean checkUseRedis(LimitNoteAnnotation limit, String key,HandlerMethod handlerMethod) {
Long increment = redisTemplate.opsForValue().increment(key, 1);
System.out.println("increment:"+increment);
String methodPath = getMethodNameWithParamterType(handlerMethod);
RefreshLimitBean build = RefreshLimitBean.builder().methodPath(methodPath).build();
RefreshLimitBean refreshLimitBean = refreshLimitBeanMapper.selectOne(build);
//第一次请求时设置缓存过期时间
if (increment == 1) {
redisTemplate.expire(key, refreshLimitBean.getTime(), TimeUnit.SECONDS);
}
if (increment > refreshLimitBean.getCount()) {
return false;
}
return true;
}
/*
获取目标方法的方法体加参数
*/
private String getMethodNameWithParamterType(HandlerMethod handlerMethod){
String beanName = handlerMethod.getResolvedFromHandlerMethod().getBeanType().getName();
Method method = handlerMethod.getMethod();
String methodName = method.getName();
Class<?>[] parameterTypes = method.getParameterTypes();
StringBuilder sb = new StringBuilder();
sb.append(beanName).append(".").append(methodName);
for (int i = 0; i < parameterTypes.length; i++) {
if (i == 0) {
sb.append("(");
}
if (i == parameterTypes.length - 1) {
sb.append(parameterTypes[i].getName()).append(")");
} else {
sb.append(parameterTypes[i].getName()).append(",");
}
}
return sb.toString();
}
}
6:定义请求超限异常
/**
* DESCRIPTION请求受限异常
*
* @author liu.qq
* @create 2019-09-09 18:26
**/
public class RequestLimitException extends RuntimeException {
}
7:注册拦截器信息到容器
package cn.arbexpress.clientmanager.aop;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
/**
* DESCRIPTION:添加拦截器到容器
*
* @author liu.qq
* @create 2019-09-10 9:59
**/
@Configuration
public class AddInterceptor extends WebMvcConfigurationSupport{
@Bean
public RequestLimitEnhanceInterceptor requestLimitAOP(){
return new RequestLimitEnhanceInterceptor();
}
@Override
protected void addInterceptors(InterceptorRegistry registry) {
super.addInterceptors(registry);
registry.addInterceptor(requestLimitAOP()).addPathPatterns("/**");//拦截所有请求
}
}
8:统一异常拦截
import cn.arbexpress.clientmanager.aop.RequestLimitException;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletResponse;
/**
* @File: GlobalExceptionHandler
* @Author: lqq
* @Date: 2018/5/14 16:03
* @Description: 统一校验异常
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler extends BaseController {
@ExceptionHandler(value = {RequestLimitException.class})
@ResponseBody
public Result requestLimit(HttpServletResponse response){
return Result.builder().errCode("5000").errMsg("请求超限").build();
}
}
9:测试代码
/**
* Copyright 2018 www.arbexpress.cn
* <p>
* All right reserved
* <p>
* Create on 2018/7/7 17:04
*/
package cn.arbexpress.clientmanager.controller.client;
import cn.arbexpress.clientmanager.aop.LimitNoteAnnotation;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* @File: TestController
* @Author: lqq
* @Date: 2018/7/7 17:04
* @Description:
*/
@Controller
public class TestController{
@RequestMapping("/test")
@LimitNoteAnnotation(desc ="测试test")
@ResponseBody
public String test(String a,Class aa) {
return "test";
}
}