Spring AOP 解决前后端恶意刷新页面和API接口服务
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测试接口
正常请求:

重定向请求:

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

纸上得来终觉浅,绝知此事要躬行。

浙公网安备 33010602011771号