Spring AOP 解决前后端恶意刷新页面和API接口服务
标签: Spring AOP 单位时间内IP限请求
Spring AOP 解决前后端恶意刷新页面和API接口服务,为了避免恶意接口请求和页面刷新。对于每个IP单位时间内次数限制,这些被认定为恶意请求,将对应访问的时间戳和次数写入Redis进行自动过期处理。
目录
对单个接口方法
定义@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测试接口
正常请求:

重定向请求:

浏览器测试页面
限制页面刷新:

智能推荐
【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...
