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

标签: Spring  AOP  单位时间内IP限请求

 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测试接口

正常请求:

重定向请求:

浏览器测试页面

限制页面刷新:

 

 

版权声明:本文为boonya原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/boonya/article/details/102998071

智能推荐

【Spring系列】IOC和AOP

Spring 环境 spring 与 maven 在maven官网中搜索spring,出现一系列的spring包供大家下载,其中 context、beans、core属于spring的核心包。其余的jar包根据你的需求来选择,相关驱动包的说明见下图。 spring 与 junit 通过maven中下载junit与spring-test的驱动包 需要注意的是你的spring-test的版本要与spr...

Spring aop原理和操作

  一:aop概念,原理,操作术语  概念: 面向切面编程,采取横线抽取机制,取代传统纵向继承体系重复性代码。 操作术语: 增强/通知:增强的逻辑,例如:日志扩展功能 连接点:类里面那些方法可以被增强,这些方法称为连接点。 切入点:类里面很多方法可以被增强,实际增强的方法称为切入点。 切面:将增强用到切入点的过程。 增强类型:  前置增强  :在方...

Spring AOP介绍和实战

摘要:我们将看到关于Spring AOP的概念以及如何实现它。 Spring AOP: 面向切面的编程是一种编程范式,类似于面向对象编程.面向对象编程的关键单元是类,类似于AOP的关键单元是切面.切面支持诸如事务管理之类的关注点的模块化,它跨越多个类和类型。它也称为横切关注点。 什么 AOP? 它提供了在业务逻辑之前、之后或周围应用关注点的可插入方法。让我们在日志的帮助下理解。您已经在不同的类中放...

Spring IOC容器和AOP

一、Spring框架就是实现了AOP功能的IOC容器;主要有以下几个模块: Test模块:支持对Spring组件在JUnit和TestNG框架中进行测试; Core Container (核心容器): 对Bean进行管理,负责Context上下文的创建与维护; AOP和Aspect :支持面向切面编程; DataAccess/Integration : 支持数据库操作,且有自己的ORM框架;还支持...

Spring IOC和AOP剖析

今天,既然讲到了Spring 的IOC和AOP,我们就必须要知道 Spring主要是两件事: 1、开发Bean;2、配置Bean。对于Spring框架来说,它要做的,就是根据配置文件来创建bean实例,并调用bean实例的方法完成“依赖注入”。 Spring框架的作用是什么?有什么优点? 1.降低了组件之间的耦合性 ,实现了软件各层之间的解耦  2.可以...

猜你喜欢

Linux内核之进程调度3:进程调度

1. 吞吐率和响应 吞吐:单位时间内做的有用功; 响应:低延迟。 吞吐追求的整个系统CPU做有用功,响应追求的是某个特定任务的延迟低; 1GHZ的CPU切换线程保存恢复现场约几个微妙级别,看似消耗不了太多时间,但是由于系统的局部性原理,会保存当前线程数据的缓存,切换线程会打乱局部性引起cache miss,而CPU访问cache速度远大于内存访问,这样综合看来上下文切换花销还是很大的。无用功占用较...

restful+ci框架 实践

restful架构: 是就是目前最流行的一种互联网软件架构。它结构清晰、符合标准、易于理解、扩展方便,所以正得到越来越多网站的采用。具体理论请看我上一篇写的restful理论。本篇主要记录下关于restful的实践。 restful实践: 工具: 这次在ci框架+restful 主要文件: 在控制器中添加控制器类:Restful.php。 在头部包含REST_Controller.php文件并继承...

Configuration, ConfigurationProperties和EnableConfigurationProperties用法

最近刚刚解决了个错误,突然又发现这个类在spring容器中找不到, 于是我就加一个 @Component的注解,哈哈直接启动成功,那我如果吧这个注解去掉,加上一个@Configuration的注解呢,哈哈还是可以的,毕竟里面已经有这个@Component的注解了。所以我就整理下Configuration,ConfigurationProperties,EnableConfigurationProp...

备战蓝桥杯--贪心算法刷题整理5

翻硬币(贪心算法) 看了一下网上的题解,感觉挺强,网友的做题思想值得借鉴,这里分享一下网友的链接,同时再分享一下自己的解题方案 链接:https://blog.csdn.net/qq_34594236/article/details/60326782 题目描述: 小明正在玩一个“翻硬币”的游戏。 桌上放着排成一排的若干硬币。我们用 * 表示正面,用 o 表示反面(是小写字母...

部署高可用RabbitMQ

安装 准备工作 这里我们使用三个RabbitMQ节点: 开通端口(具体见官方文档): 安装ErLang和RabbitMQ Server 安装文档见:https://www.rabbitmq.com/install-rpm.html。 采用RPM包而不是Repo的安装命令如下(以下的版本号可根据实际情况修改): 安装管理插件 安装文档见:https://www.rabbitmq.com/manage...