浙江的八月下旬还是有点热
实现目标:在接口上面加一个注解。限制单个ip在指定时间范围内可以访问的次数。
实现的逻辑是,将访问的ip和要访问的url作为key存放在reids中。
设定其数据类型为list,value的值为每次访问的时间戳。
redis中的数据如图:
验证方法:
当list的长度达到了设定的访问最大次数,
就和用当前的时间戳和最早存放的时间戳做对比。
若相差时间小于设定的时间范围,则说明此ip访问此接口达到了上限。
开始实现
新建自定义注解用在controller中需要限制的接口上面
import java.lang.annotation.*;
/**
* ip 最大 访问次数
* time 时间范围
* (@interface 注解类)
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface IpMax {
/**
* 允许访问的最大次数
*/
int count() default Integer.MAX_VALUE;
/**
* 时间段,单位为秒,默认值三十秒
*/
int time() default 30;
}
使用如下, 直接在原有接口上面添加刚刚定义好的注解。其中 count 为最大访问次数,time为时间范围(本处时间单位采用的是秒)
@IpMax(count = 3, time = 10)
@ApiOperation("查询用户的数量")
@PostMapping("/getUserCount")
public long getUserCount() {
return userService.getUserCount();
}
在拦截器中添加ip是否超频的验证逻辑
import com.batata.continuing.config.changeable.OpenOrClose;
import com.batata.continuing.config.count.ip.IpCount;
import com.batata.continuing.config.count.ip.IpMax;
import com.batata.continuing.config.moreNote.token.NotNeedToken;
import com.batata.continuing.utils.JwtUtil;
import org.nutz.lang.Lang;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
public class JwtInterceptor implements HandlerInterceptor {
@Autowired
private OpenOrClose openOrClose;
/**
* 用来验证ip访问次数的逻辑层
*/
@Autowired
private IpCount ipCount;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object object) {
// 从 http 请求头中取出 token
String token = request.getHeader("token");
// 如果不是映射到方法直接通过
if (!(object instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) object;
Method method = handlerMethod.getMethod();
// ip超频访问方面的验证 start
if (method.isAnnotationPresent(IpMax.class)) { // 判断访问的接口是否有此注解
String requestURI = request.getRequestURI(); // 请求的url
String ip = Lang.getIP(request); // 获得请求者的ip(可根据自己的方法,我这边用的是nutz的工具类,引入请参考历史文章)
IpMax ipMax = method.getAnnotation(IpMax.class); // 获得注解中的内容
int count = ipMax.count(); // 访问次数
long time = ipMax.time(); // 时间范围
// 通过封装的方法,判断ip是否可以通过验证
boolean ipIsOk = ipCount.ipIsOk(requestURI, ip, count, time);
if (!ipIsOk) { // 超过访问次数
// 时间范围内超出最大访问次数 注:超过访问次数的处理方式可自行根据具体需求
throw new RuntimeException("本接口" + time + "秒内可以请求" + count + "次,您已超出最大访问次数!!!");
}
}
// ip超频访问方面的验证 end
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
}
ip验证的逻辑处理
import com.batata.continuing.config.RedisKeyConfig;
import com.batata.continuing.service.common.RedisService;
import com.batata.continuing.utils.DateUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* ip 相关的统计
*/
@Component
public class IpCount {
/**
* 封装的redis方法
*/
@Autowired
private RedisService redisService;
@Autowired
public RedisTemplate redisTemplate;
/**
* ip 是否可以继续访问
*
* @param url 访问的url
* @param ip 访问的ip
* @param maxValue 最大的访问次数
* @param time 时间范围 单位为秒
* @return true 可以继续访问 false 超出限制,不可以继续访问
*/
public boolean ipIsOk(String url, String ip, int maxValue, long time) {
boolean ipIsOk = true;
String key = RedisKeyConfig.ipCount + ip + url; // 根据指定规则拼接生成此次访问的key
List<Long> timeList = redisService.getCacheList(key); // 查询redis中已有的数据
if (
timeList.size() == 0 || timeList.size() < maxValue
) { // 没有记录或者没有达到最大访问次数不做超时验证
addNewTime(key, time); // 添加当前的时间到list中
return ipIsOk;
} // 若不满足此条件,则证明list中的值达到了最大数量(即访问的次数)
// 判断达到规定的访问次数的用时是否小于规定的时间
// (当前时间戳-最旧记录的时间戳)< 限定的时间转毫秒
if (
(DateUtils.getNowTimeLong() - timeList.get(0)) < (time * 1000)
) {
// 未达到,证明指定范围时间内访问数量超过的定义数量
ipIsOk = false;
}
// 删除第一个值(就是时间最旧的那个值,我这边是下标为0的,手动在redis客户端测试的为row最大的值。这个根据自己的具体情况)
redisTemplate.opsForList().remove(key, 1, timeList.get(0));
addNewTime(key, time); // 添加当前的时间到list中
return ipIsOk;
}
/**
* 往redis中添加新的数据,注:新增的值row在后
*
* @param key key
* @param time 有效时间,单位为秒
*/
public void addNewTime(String key, long time) {
List<Long> nowTime = new ArrayList<>();
nowTime.add(DateUtils.getNowTimeLong()); // 当前时间戳
redisService.setCacheList(key, nowTime); // 追加值或新缓存值
redisService.expire(key, time + 1); // 设置有效时间
}
}
// 用到的工具类 start
redis操作封装的service (仅含使用到的方法)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.TimeUnit;
@SuppressWarnings(value = {"unchecked", "rawtypes"})
@Component
public class RedisService {
@Autowired
public RedisTemplate redisTemplate;
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间 注:此处时间单位为秒
* @return true=设置成功;false=设置失败
*/
public boolean expire(final String key, final long timeout) {
return expire(key, timeout, TimeUnit.SECONDS);
}
/**
* 缓存List数据
*
* @param key 缓存的键值
* @param dataList 待缓存的List数据
* @return 缓存的对象
*/
public <T> long setCacheList(final String key, final List<T> dataList) {
Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
return count == null ? 0 : count;
}
/**
* 获得缓存的list对象
*
* @param key 缓存的键值
* @return 缓存键值对应的数据
*/
public <T> List<T> getCacheList(final String key) {
return redisTemplate.opsForList().range(key, 0, -1);
}
}
时间util(仅含使用到的方法)
import java.text.DecimalFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import org.apache.commons.lang3.time.DateFormatUtils;
public class DateUtils {
/**
* 获得当前时间的时间戳
* @return 当前时间戳
*/
public static long getNowTimeLong() {
return System.currentTimeMillis();
}
}
// 用到的工具类 end
启动项目访问加了注解的接口测试
第四次访问出现请求不通过,(此处的处理方式为抛出了一个异常)配置成功
查看redis中的数据为
另注:也可以配合超频做一个黑名单的机制