接口防刷
- 一、全局接口防刷(通过拦截器方式)
- 1、原理 + 代码示例
- 二、个别接口防刷(接口注解方式)
- 1、代码示例
一、全局接口防刷(通过拦截器方式)
1、原理 + 代码示例
- 通过
ip地址+uri拼接
用以作为访问者访问接口区分 - 通过在
Interceptor中拦截请求
,从Redis中统计用户访问接口次数从而达到接口防刷目的
拦截器:
package org.jeecg.config.InterceptorLimint;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@Slf4j
public class AccessLimintInterceptor implements HandlerInterceptor {
@Resource
private RedisTemplate<String, Object> redisTemplate;
/**
* 多长时间内
*/
@Value("${interfaceAccess.second}")
private Long second = 10L;
/**
* 访问次数
*/
@Value("${interfaceAccess.requestCount}")
private Long requestCount = 3L;
/**
* 禁用时长--单位/秒
*/
@Value("${interfaceAccess.lockTime}")
private Long lockTime = 60L;
/**
* 锁住时的key前缀
*/
public static final String LOCK_PREFIX = "LOCK";
/**
* 统计次数时的key前缀
*/
public static final String COUNT_PREFIX = "COUNT";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uri = request.getRequestURI();
String ip = request.getRemoteAddr(); // 这里忽略代理软件方式访问,默认直接访问,也就是获取得到的就是访问者真实ip地址
String lockKey = LOCK_PREFIX + ip + uri;
Object isLock = redisTemplate.opsForValue().get(lockKey);
if(Objects.isNull(isLock)){
// 还未被禁用
String countKey = COUNT_PREFIX + ip + uri;
Object count = redisTemplate.opsForValue().get(countKey);
if(Objects.isNull(count)){
// 首次访问
log.info("首次访问");
redisTemplate.opsForValue().set(countKey,1,second, TimeUnit.SECONDS);
}else{
// 此用户前一点时间就访问过该接口
if((Integer)count < requestCount){
// 放行,访问次数 + 1
redisTemplate.opsForValue().increment(countKey);
}else{
log.info("{}禁用访问{}",ip, uri);
// 禁用
redisTemplate.opsForValue().set(lockKey, 1,lockTime, TimeUnit.SECONDS);
// 删除统计
redisTemplate.delete(countKey);
throw new RuntimeException("请勿点击那么快,稍等一下!");
}
}
}else{
// 此用户访问此接口已被禁用
throw new RuntimeException("请勿点击那么快,稍等一下!");
}
return true;
}
}
yml配置限制参数
# 自定义接口拦截配置
interfaceAccess:
second: 10 # 接口访问second秒内最多只能访问requestCount次
requestCount: 3 # 请求3次
lockTime: 20 # 禁用时长--单位/秒
WebMvcConfiguration 配置类
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
@Bean
public AccessLimintInterceptor createAccessLimintInterceptor(){
return new AccessLimintInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(createAccessLimintInterceptor())
.addPathPatterns("/**") // 全部接口
.excludePathPatterns("/sys/test"); // 去除某些不需要限制的接口
}
}
此时就已经能实现限制接口访问了。
不足
:不能针对接口做处理,某些接口操作频次高,他的请求拦截配置就要配高点,有些低的就配低点,
为了解决这个不足我们下面添加了针对接口注解的方式
二、个别接口防刷(接口注解方式)
1、代码示例
1、定义注解
/**
* 接口防刷
*/
@Retention(RUNTIME)
@Target({METHOD, TYPE})
public @interface AccessLimit {
/**
* 秒
* @return 多少秒内
*/
long second() default 5L;
/**
* 最大访问次数
* @return 最大访问次数
*/
long maxRequestCount() default 3L;
/**
* 禁用时长,单位/秒
* @return 禁用时长
*/
long forbiddenTime() default 120L;
}
2、切面实现类
package org.jeecg.config.InterceptorLimint;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
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.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* 防刷切面实现类
*/
@Aspect
@Component
@Slf4j
public class AccessLimitAspect {
@Resource
private RedisTemplate<String, Object> redisTemplate;
/**
* 锁住时的key前缀
*/
public static final String LOCK_PREFIX = "LOCK";
/**
* 统计次数时的key前缀
*/
public static final String COUNT_PREFIX = "COUNT";
/**
* 定义切点Pointcut
*/
@Pointcut("execution(public * org.jeecg.modules..*.*Controller.*(..)) || @annotation(org.jeecg.config.InterceptorLimint.AccessLimit)")
public void excudeService() {
}
// 前置通知、在切点方法之前执行
@Before("excudeService()")
public void doAround(JoinPoint joinPoint) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
AccessLimit pd = method.getAnnotation(AccessLimit.class);
long second = pd.second();
long maxRequestCount = pd.maxRequestCount();
long forbiddenTime = pd.forbiddenTime();
String ip = request.getRemoteAddr();
String uri = request.getRequestURI();
if (isForbindden(second, maxRequestCount, forbiddenTime, ip, uri)) {
throw new RuntimeException("请勿点击那么快,稍等一下!");
}
}
/**
* 判断某用户访问某接口是否已经被禁用/是否需要禁用
*
* @param second 多长时间 单位/秒
* @param maxRequestCount 最大访问次数
* @param forbiddenTime 禁用时长 单位/秒
* @param ip 访问者ip地址
* @param uri 访问的uri
* @return ture为需要禁用
*/
private boolean isForbindden(long second, long maxRequestCount, long forbiddenTime, String ip, String uri) {
String lockKey = LOCK_PREFIX + ip + uri; //如果此ip访问此uri被禁用时的存在Redis中的 key
Object isLock = redisTemplate.opsForValue().get(lockKey);
// 判断此ip用户访问此接口是否已经被禁用
if (Objects.isNull(isLock)) {
// 还未被禁用
String countKey = COUNT_PREFIX + ip + uri;
Object count = redisTemplate.opsForValue().get(countKey);
if (Objects.isNull(count)) {
// 首次访问
log.info("首次访问");
redisTemplate.opsForValue().set(countKey, 1, second, TimeUnit.SECONDS);
} else {
// 此用户前一点时间就访问过该接口,且频率没超过设置
if ((Integer) count < maxRequestCount) {
redisTemplate.opsForValue().increment(countKey);
} else {
log.info("{}禁用访问{}", ip, uri);
// 禁用
redisTemplate.opsForValue().set(lockKey, 1, forbiddenTime, TimeUnit.SECONDS);
// 删除统计--已经禁用了就没必要存在了
redisTemplate.delete(countKey);
return true;
}
}
} else {
// 此用户访问此接口已被禁用
return true;
}
return false;
}
}
3、使用注解
@AccessLimit(second = 10,maxRequestCount = 3,forbiddenTime = 20)
@RequestMapping(value = "/test", method = RequestMethod.GET)
public Result<JSONObject> test(){
return Result.OK();
}
测试效果