天下之事,必先处之难,而后易之。

Spring AOP 解决前后端恶意刷新页面和API接口服务

 Spring AOP 解决前后端恶意刷新页面和API接口服务,为了避免恶意接口请求和页面刷新。对于每个IP单位时间内次数限制,这些被认定为恶意请求,将对应访问的时间戳和次数写入Redis进行自动过期处理。

目录

对单个接口方法

定义@LimitMethod注解

定义LimitReqHandler计数器

定义LimitMethodReqAspect 切面组件

使用@LimitMethod注解 

设计通用类和方法的注解

定义@LimitAPI注解

定义LimitAPIReqAspect切面组件

使用@LimitAPI注解

测试AOP 接口页面限IP请求

spring-servlet.xml配置AOP

Jmeter测试接口

浏览器测试页面


对单个接口方法

定义@LimitMethod注解

package com.boonya.limitreq.apilimit.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.stereotype.Component;
/**
 * 
 * @function  功能:限制单位时间内请求单个方法接口次数
 * @author    PJL
 * @package   com.forestar.aop.annotation
 * @filename  LimitMethod.java
 * @time      2019年11月7日 下午3:03:43
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Component
public @interface LimitMethod {
	
	/**
	 * 限制请求次数
	 * @return
	 */
	 int limitTimes() default 10;
	 
    /**
	 * 限制单位时间(毫秒/ms)
	 * @return
	 */
	 int milliseconds() default 60000;

}

定义LimitReqHandler计数器

用Atomic*原子类型是为了解决多线程并发修改问题。

package com.boonya.limitreq.apilimit.annotation;


import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

/**
 *
 * @function  功能:计数和统计访问次数
 * @author    PJL
 * @package   com.boonya.limitreq.apilimit.annotation
 * @filename  LimitReqHandler.java
 * @time      2019年11月10日 下午3:28:11
 */
public class LimitReqHandler {

    private AtomicInteger sequenceId=new AtomicInteger(1);

	private AtomicInteger requestCount=new AtomicInteger(1);
	
	private AtomicLong lastRequestTime=new AtomicLong(System.currentTimeMillis());

    /**
     * 递增序列和请求次数
     */
    public  void increaseCount() {
        requestCount.incrementAndGet();
        sequenceId.incrementAndGet();
    }

    /**
     * 获取序列(不超过最大请求次数)
     * @return
     */
    public AtomicInteger getSequenceId() {
        return sequenceId;
    }

    /**
     * 设置序列(重置序列验证是否从新开始)
     * @return
     */
    public synchronized void setSequenceId(AtomicInteger sequenceId) {
        this.sequenceId = sequenceId;
    }

    /**
     * 获取执行次数(单位时间内请求次数)
     * @return
     */
    public AtomicInteger getRequestCount() {
        return requestCount;
    }

    /**
     * 设置或重置执行次数(单位时间内请求次数)
     * @return
     */
    public synchronized void setRequestCount(AtomicInteger requestCount) {
        this.requestCount = requestCount;
    }

    /**
     * 获取最后请求时间戳
     * @return
     */
    public AtomicLong getLastRequestTime() {
        return lastRequestTime;
    }

    /**
     * 设置或重置最后请求时间戳
     * @return
     */
    public synchronized void setLastRequestTime(AtomicLong lastRequestTime) {
        this.lastRequestTime = lastRequestTime;
    }


}

定义LimitMethodReqAspect 切面组件

package com.boonya.limitreq.apilimit.aop;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import com.alibaba.fastjson.JSON;
import com.boonya.limitreq.apilimit.annotation.LimitMethod;
import com.boonya.limitreq.apilimit.annotation.LimitReqHandler;
import com.boonya.limitreq.apilimit.util.HttpRequetUrl;
import com.boonya.limitreq.apilimit.util.RedisUtil;
import com.boonya.limitreq.apilimit.util.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
/**
 * 
 * @function  功能:AOP限制每个IP调用指定的方法次数
 * @author    PJL
 * @package   com.boonya.limitreq.apilimit.aop
 * @filename  LimitIPReqAspect.java
 * @time      2019年11月8日 下午3:24:07
 */
@Aspect
@Component
public class LimitMethodReqAspect {

	private Logger logger = LoggerFactory.getLogger(LimitMethodReqAspect.class);

	public static final String REDIS_LIMIT_KEY="xht:limit:method";

	/**
	 * 切点:自定义注解@LimitReq
	 */
	@Pointcut("@annotation(com.boonya.limitreq.apilimit.annotation.LimitMethod)")
	public void limitMethod() {}

	/**
	 * 切点调用时机
	 * @param joinPoint
	 */
	@Before("limitMethod()")
	public void before(JoinPoint joinPoint) {
		HttpServletRequest request=((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
		HttpServletResponse response = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getResponse();
		HttpSession session = request.getSession();
		Signature signature = joinPoint.getSignature();
		MethodSignature methodSignature = (MethodSignature) signature;
		Method method = methodSignature.getMethod();
		LimitMethod req = method.getAnnotation(LimitMethod.class);
		String ip = HttpRequetUrl.getIpAddr(request);

		logger.info("LIMIT_TIME : " + req.milliseconds());
		logger.info("URL : " + request.getRequestURL().toString());
		logger.info("HTTP_METHOD : " + request.getMethod());
		logger.info("IP : " + ip);
		logger.info("CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
		logger.info("ARGS : " + Arrays.toString(joinPoint.getArgs()));
		// 设置IP请求的方法映射KEY
        String key=REDIS_LIMIT_KEY+":"+ip+":"+method.getName();
        logger.info("REDIS_KEY : " + key);
        String jsonData= RedisUtil.hget(key, ip);
		if (StringUtils.IsNullOrEmpty(jsonData)) {
			LimitReqHandler dto = new LimitReqHandler();
			dto.setLastRequestTime(new AtomicLong(System.currentTimeMillis()));
			dto.setRequestCount(new AtomicInteger(1));
			setRedisData(key, ip, req, dto);
			logger.info(session.getId()+":new client call.");
		} else {
			this.updateData(key, ip, req, jsonData,response);
		}
	}
	/**
	 * 更新REDIS内存数据
	 * @param key
	 * @param ip
	 * @param req
	 * @param dto
	 */
	private void setRedisData(String key,String ip,LimitMethod req,LimitReqHandler dto){
		RedisUtil.hset(key, ip, JSON.toJSONString(dto));
		RedisUtil.expire(key, req.milliseconds()/1000);
	}
	
	/**
	 * 统计单位时间内客户端请求重复次数
	 * @param key
	 * @param ip
	 * @param req
	 * @param jsonData
	 * @param response
	 */
	private synchronized void updateData(String key,String ip,LimitMethod req,String jsonData,HttpServletResponse response){
		LimitReqHandler dto = JSON.parseObject(jsonData, LimitReqHandler.class);
		logger.info("限制请求次数序列:"+dto.getSequenceId()+" 单位时间内请求总次数:"+dto.getRequestCount());
		dto.increaseCount();
		// 超时重置时间
		long time = System.currentTimeMillis()- dto.getLastRequestTime().longValue();
		if (time >= req.milliseconds()) {
			dto.setSequenceId(new AtomicInteger(1));
			dto.setRequestCount(new AtomicInteger(1));
			dto.setLastRequestTime(new AtomicLong(System.currentTimeMillis()));
		}
		// 最大次数大于限制值
		if (dto.getRequestCount().intValue() > req.limitTimes()) {
			try{
				// 序列超过最大值时重置
				if(dto.getSequenceId().intValue()> req.limitTimes()){
					dto.setSequenceId(new AtomicInteger(1));
				}
				setRedisData(key, ip, req, dto);
				if(!StringUtils.IsNullOrEmpty(response)){
					response.sendRedirect("busy.do");
				}
				logger.info("Specially:redirect to busy.do page. " );
			}catch (Exception e){
				e.printStackTrace();
			}
		}else{
			setRedisData(key, ip, req, dto);
		}
		
	}

}

使用@LimitMethod注解 

package com.boonya.limitreq.apilimit.controller;

import com.boonya.limitreq.apilimit.annotation.LimitMethod;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class LoginController {

    @RequestMapping({"/busy.do"})
    public String busy() {
        return "busy";
    }

    @RequestMapping({"/login.do"})
    public String login() {
        return "index";
    }

    @LimitMethod
    @RequestMapping({"/index.do"})
    public String index() {
        return "index";
    }
}

设计通用类和方法的注解

定义@LimitAPI注解

package com.boonya.limitreq.apilimit.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.stereotype.Component;
/**
 * 
 * @function  功能:限制单位时间内请求类下API接口次数(@LimitMethod增强可配置类和方法)
 * @author    PJL
 * @package   com.boonya.limitreq.apilimit.annotation
 * @filename  LimitAPI.java
 * @time      2019年11月7日 下午3:03:43
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
@Component
public @interface LimitAPI {
	
	/**
	 * 限制请求次数
	 * @return
	 */
	 int limitTimes() default 10;
	 
    /**
	 * 限制单位时间(毫秒/ms)
	 * @return
	 */
	 int milliseconds() default 60000;
	 
	 /**
	  * 频繁请求重定向地址
	  * @return
	  */
	 String busyRedirectUrl() default "";

}

定义LimitAPIReqAspect切面组件

package com.boonya.limitreq.apilimit.aop;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import com.alibaba.fastjson.JSON;
import com.boonya.limitreq.apilimit.annotation.LimitAPI;
import com.boonya.limitreq.apilimit.annotation.LimitReqHandler;
import com.boonya.limitreq.apilimit.util.HttpRequetUrl;
import com.boonya.limitreq.apilimit.util.RedisUtil;
import com.boonya.limitreq.apilimit.util.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
/**
 * 
 * @function  功能:AOP限制每个IP调用指定的方法次数
 * @author    PJL
 * @package   com.boonya.limitreq.apilimit.aop
 * @filename  LimitIPReqAspect.java
 * @time      2019年11月8日 下午3:24:07
 */
@Aspect
@Component
public class LimitAPIReqAspect {

	private Logger logger = LoggerFactory.getLogger(LimitAPIReqAspect.class);

	public static final String REDIS_LIMIT_KEY="xht:limit:api";

	/**
	 * 切点:URL映射@RequestMapping
	 */
	@Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
	public void limitAPI() {}

	/**
	 * 切点调用时机
	 * @param joinPoint
	 * @throws Exception 
	 */
	@Before("limitAPI()")
	public void before(JoinPoint joinPoint) throws Exception {
		HttpServletRequest request=((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
		HttpServletResponse response =((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getResponse();
		HttpSession session = request.getSession();
		Signature signature = joinPoint.getSignature();
		MethodSignature methodSignature = (MethodSignature) signature;
		Method method = methodSignature.getMethod();
		LimitAPI limit=null;
		// 从类上获取注解
		LimitAPI limitClazz = AnnotationUtils.findAnnotation(method.getDeclaringClass(), LimitAPI.class);
		if(null==limitClazz){
			// 从方法上获取注解
			limit = AnnotationUtils.findAnnotation(method, LimitAPI.class);
		}else{
			limit=limitClazz;
		}
		
		// 没有配置@LimitAPI的请求不做限制
		if(null==limit){
			logger.info("API request is not limit.");
			return ;
		}
		// 检查接口类是否设置从定向地址
		if(StringUtils.IsNullOrEmpty(limit.busyRedirectUrl())){
			throw new Exception("API接口@Controller类@LimitAPI busyRedirectUrl必须设置错误重定向接口配置地址或页面!");
		}
		
		RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
		String ip = HttpRequetUrl.getIpAddr(request);
		String url=request.getRequestURL().toString();
		
		// 排除AOP限请求重定向接口
		if(url.contains(limit.busyRedirectUrl())){
			logger.info("API busyRedirectUrl '"+limit.busyRedirectUrl()+"' url not limit.");
			return ;
		}

		logger.info("LIMIT_TIME : " + limit.milliseconds());
		logger.info("URL : " + url);
		logger.info("HTTP_METHOD : " + request.getMethod());
		logger.info("MAPPING_VALUE : " + requestMapping.value()[0]);
		logger.info("IP : " + ip);
		logger.info("CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
		logger.info("ARGS : " + Arrays.toString(joinPoint.getArgs()));
		// 设置IP对应的请求地址次数
    	String key=REDIS_LIMIT_KEY+":"+ip+":"+requestMapping.value()[0];
    	logger.info("REDIS_KEY : " + key);
        String jsonData= RedisUtil.hget(key, ip);
  		if (StringUtils.IsNullOrEmpty(jsonData)) {
  			LimitReqHandler dto = new LimitReqHandler();
  			dto.setLastRequestTime(new AtomicLong(System.currentTimeMillis()));
  			dto.setRequestCount(new AtomicInteger(1));
  			setRedisData(key, ip, limit, dto);
  			logger.info(session.getId()+":new client call.");
  		} else {
  			this.updateData(key, ip, limit, jsonData,response);
  		}
	}
	
	/**
	 * 更新REDIS内存数据
	 * @param key
	 * @param ip
	 * @param limit
	 * @param dto
	 */
	private void setRedisData(String key,String ip,LimitAPI limit,LimitReqHandler dto){
		RedisUtil.hset(key, ip, JSON.toJSONString(dto));
		RedisUtil.expire(key, limit.milliseconds()/1000);
	}
	
	/**
	 * 统计单位时间内客户端请求重复次数
	 * @param key
	 * @param ip
	 * @param limit
	 * @param jsonData
	 * @param response
	 */
	private synchronized void updateData(String key,String ip,LimitAPI limit,String jsonData,HttpServletResponse response){
		LimitReqHandler dto = JSON.parseObject(jsonData, LimitReqHandler.class);
		logger.info("限制请求次数序列:"+dto.getSequenceId()+" 单位时间内请求总次数:"+dto.getRequestCount());
		dto.increaseCount();
		// 超时重置时间
		long time = System.currentTimeMillis()- dto.getLastRequestTime().longValue();
		if (time >= limit.milliseconds()) {
			dto.setSequenceId(new AtomicInteger(1));
			dto.setRequestCount(new AtomicInteger(1));
			dto.setLastRequestTime(new AtomicLong(System.currentTimeMillis()));
		}
		// 最大次数大于限制值
		if (dto.getRequestCount().intValue() > limit.limitTimes()) {
			try{
				// 序列超过最大值时重置
				if(dto.getSequenceId().intValue()> limit.limitTimes()){
					dto.setSequenceId(new AtomicInteger(1));
				}
				setRedisData(key, ip, limit, dto);
				logger.info("Specially:redirect to busy.do page. " );
				// 设置重定向页面或数据接口
				response.sendRedirect(limit.busyRedirectUrl());
				return ;
			}catch (Exception e){
				e.printStackTrace();
			}
		}else{
			setRedisData(key, ip, limit, dto);
		}
		
	}

}

使用@LimitAPI注解

package com.boonya.limitreq.apilimit.controller;

import com.boonya.limitreq.apilimit.annotation.LimitAPI;
import com.boonya.limitreq.apilimit.annotation.LimitMethod;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@LimitAPI(busyRedirectUrl = "busy.do")
public class LoginController {

    @RequestMapping({"/busy.do"})
    public String busy() {
        return "busy";
    }

    @RequestMapping({"/login.do"})
    public String login() {
        return "index";
    }

   // @LimitMethod
    @RequestMapping({"/index.do"})
    public String index() {
        return "index";
    }
}

AOP 接口页面限IP请求测试

截图均来自具体项目。

spring-servlet.xml配置AOP

<!-- AOP动态代理必须在spring-servlet.xml中配置 -->
    <aop:aspectj-autoproxy />

注:需要加上AOP配置。

Jmeter测试接口

正常请求:

重定向请求:

浏览器测试页面

限制页面刷新:

 

 

posted @ 2023-08-09 15:36  boonya  阅读(19)  评论(0)    收藏  举报  来源
我有佳人隔窗而居,今有伊人明月之畔。
轻歌柔情冰壶之浣,涓涓清流梦入云端。
美人如娇温雅悠婉,目遇赏阅适而自欣。
百草层叠疏而有致,此情此思怀彼佳人。
念所思之唯心叩之,踽踽彳亍寤寐思之。
行云如风逝而复归,佳人一去莫知可回?
深闺冷瘦独自徘徊,处处明灯影还如只。
推窗见月疑是归人,阑珊灯火托手思忖。
庐居闲客而好品茗,斟茶徐徐漫漫生烟。

我有佳人在水之畔,瓮载渔舟浣纱归还。
明月相照月色还低,浅近芦苇深深如钿。
庐山秋月如美人衣,画堂春阁香气靡靡。
秋意幽笃残粉摇曳,轻轻如诉画中蝴蝶。
泾水潺潺取尔浇园,暮色黄昏如沐佳人。
青丝撩弄长裙翩翩,彩蝶飞舞执子手腕。
香带丝缕缓缓在肩,柔美体肤寸寸爱怜。
如水之殇美玉成欢,我有佳人清新如兰。
伊人在水我在一边,远远相望不可亵玩。