上周末公司线上环境出问题了,某个服务容器老是崩溃。忧心忡忡的排查一番后,发现是有个接口在被爬虫轮询扒数据,之前业务原因这个接口没有身份验证,临时加上了,也查到对方几个ip封掉。公司开会计划年后前后端加入严格点的加密验证,年前让我暂时在后端做一下优化(前端有app,让客户年前更新成本太大)。
思路是这样的:对于所有接口 同一个请求地址,并且同一个ip, 5秒内最多请求5次。超过的话提示访问过于频繁,5秒无请求后恢复,有请求重新延长5秒。
其实google的Guava包 令牌桶RateLimiter类 已经很好了,不过只适合单应用模式,需要修改。就没有使用它了。下面是自己通过redis保存时间戳的方式 简单的控制了请求频率:
/**
* ip接口访问记录对象
* @Auther: Administrator
* @Date: 2019/1/30 15:50
* @Description:
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AccessTimeOut {
//同ip同接口首次请求的时间戳
private long timestamp;
//时间段内的访问次数
private int count;
}
需要缓存的对象,本人最近刚用上的lombok包,代码简洁不少。
/**
* 缓存接口访问信息
* @Auther: Administrator
* @Date: 2019/1/30 10:59
* @Description:
*/
@Service
public class RedisService {
@Cacheable(value = "accesscount", key = "'accesscount_'+#key")
public AccessTimeOut get(String key){
AccessTimeOut timeOut = new AccessTimeOut(DateUtils.nowTimeMillis(),0);
return timeOut;
}
@CachePut(value = "accesscount", key = "'accesscount_'+#key")
public AccessTimeOut update(String key,AccessTimeOut timeOut){
timeOut.setCount(timeOut.getCount()+1);
return timeOut;
}
@CacheEvict(value = "accesscount", key = "'accesscount_'+#key")
public void del(String key) {
}
}
这是缓存操作,这网关应用只有此处用到redis,所以名字起得随意。
DateUtils.nowTimeMillis()方法就是封装的System.currentTimeMillis()
然后以下所有代码都是网关的拦截器类里面,隐去了不相关的业务代码
@Component
public class AccessFilter extends ZuulFilter {
//时间段内(缓存有效期)同一IP对相同接口的最大访问次数
final static int MAX_ACCESS_COUNT = 5;
final static int MAX_ACCESS_TIMEOUT = 5000;
final static org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(AccessFilter.class);
@Autowired
RedisService redisService;
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return FORM_BODY_WRAPPER_FILTER_ORDER - 1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String token = request.getHeader("token");
if (token == null) {
token = request.getParameter("token");
}
if(accessIsOut(request.getServletPath(),CommonUtil.getIpAddr(request),token)){
//判断ip是否超过接口访问频率
logger.warn("访问超过次数!");
HttpServletResponse response = ctx.getResponse();
response.setCharacterEncoding("utf-8"); //设置字符集
response.setContentType("text/html; charset=utf-8"); //设置相应格式
response.setStatus(200);
ctx.setSendZuulResponse(false); //不进行路由
try {
response.getWriter().print("{\"code\":\"416\",\"message\":\"短时间内请求过多!\"}"); //响应体
} catch (IOException e) {
e.printStackTrace();
}
ctx.setResponse(response);
return null;
}
······略去其他无关代码
ctx.setSendZuulResponse(true);// 对该请求进行路由
return null;
}
private boolean accessIsOut(String address,String ip,String token){
logger.info(String.format("请求:[%s][%s][%s]",address,ip,token));
//去掉最后一个/后面内容,以去掉大部分get请求参数
String key = (address.length()<30?address:address.substring(0,address.lastIndexOf("/")))+"_"+ip;
AccessTimeOut oldTimeOut = redisService.get(key);
AccessTimeOut timeOut = redisService.update(key,oldTimeOut);
if(timeOut.getCount()>MAX_ACCESS_COUNT){
if(DateUtils.nowTimeMillis() - timeOut.getTimestamp()<MAX_ACCESS_TIMEOUT) {
//从第一次计数到超出,是否还在5秒内
timeOut.setTimestamp(DateUtils.nowTimeMillis());
redisService.update(key,timeOut);
return true;
}
redisService.del(key);
}
return false;
}
}
另外上面获取ip的方法如下,一些请求是代理转发过来的可能需要这样才能拿到真实ip:
public static String getIpAddr(HttpServletRequest request){
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}else {
if (ip.indexOf(",") > 0) {
ip = ip.split(",")[0];
}
}
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();
}
return ip;
}
以上就是所有内容,已经上线测试环境一天了对业务没什么影响,只是功能有点弱鸡。。
年前和过年这段时间还是以人为监控+封ip为主吧,应该问题不大。
补充:今天被老大给否了,说生产环境记好日志就行