目录
一、问题
在项目中,接口的暴露在外面,很多人就会恶意多次快速请求,那我们开发的接口和服务器在这样的频率下的话,服务器和数据库很快会奔溃的,那我们该怎么防止接口防刷呢?
1、解决
其实也就是spring拦截器来实现。在需要防刷的方法上,加上防刷的注解,拦截器拦截这些注解的方法后,进行接口存储到redis中。当用户多次请求时,我们可以累积他的请求次数,达到了上限,我们就可以给他提示错误信息。
2、原理
在你请求的时候,服务器通过redis 记录下你请求的次数,如果次数超过限制就不给访问。 在redis 保存的key 是有时效性的,过期就会删除。
我们使用注解的形式来实现这样一个效果
二、实现
这里我使用拦截器和Redis实现接口请求限制
1、导入坐标
导入redis相关坐标和json
并且在配置文件中添加redis连接信息
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.32</version>
</dependency>
<!--json依赖-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.14.1</version>
</dependency>
2、自定义注解
创建自定义的注解@RequestLimit
请求限制的自定义注解
@Target 注解可修饰的对象范围,ElementType.METHOD 作用于方法,ElementType.TYPE 作用于类
(ElementType)取值有:
1.CONSTRUCTOR:用于描述构造器
2.FIELD:用于描述域
3.LOCAL_VARIABLE:用于描述局部变量
4.METHOD:用于描述方法
5.PACKAGE:用于描述包
6.PARAMETER:用于描述参数
7.TYPE:用于描述类、接口(包括注解类型) 或enum声明
@Retention定义了该Annotation被保留的时间长短:某些Annotation仅出现在源代码中,而被编译器丢弃;
而另一些却被编译在class文件中;编译在class文件中的Annotation可能会被虚拟机忽略,
而另一些在class被装载时将被读取(请注意并不影响class的执行,因为Annotation与class在使用上是被分离的)。
使用这个meta-Annotation可以对 Annotation的“生命周期”限制。
(RetentionPoicy)取值有:
1.SOURCE:在源文件中有效(即源文件保留)
2.CLASS:在class文件中有效(即class保留)
3.RUNTIME:在运行时有效(即运行时保留)
@Inherited
元注解是一个标记注解,@Inherited阐述了某个被标注的类型是被继承的。
如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。
max 最大的请求数、second 代表时间,单位是秒
默认5秒内请求2次
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.TYPE})
@Documented
@Inherited
@JacksonAnnotationsInside
public @interface RequestLimit {
// 在 second 秒内,最大只能请求 maxCount 次
/**
* 限定时间
*/
long seconds() default 5L;
/**
* 限定请求次数
*/
long max() default 2L;
}
3、Redis 缓存工具类
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import javax.annotation.Resource;
import java.util.Collection;
import java.util.concurrent.TimeUnit;
@Component
public class RedisUtils {
@Resource
private RedisTemplate<String, Object> redisTemplate;
/**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒)
* @return
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
*
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
*
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key));
}
}
}
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, String value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, int value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, String.valueOf(value));
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
*
* @param key 键
* @param delta 要增加几(大于0)
* @return
*/
public long incr(String key, long delta) {
if (delta < 0) {
return -1;
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
*
* @param key 键
* @param delta 要减少几(小于0)
* @return
*/
public long decr(String key, long delta) {
if (delta < 0) {
return -1;
}
return redisTemplate.opsForValue().increment(key, -delta);
}
}
4、自定义拦截器
自定义一个拦截器,请求之前,进行请求次数校验
创建一个拦截器,继承HandlerInterceptorAdapter,在preHandle方法中做具体的操作。
每次请求都会根据key查询redis获取其访问次数,如果没有则是第一次访问,往redis中插入数据,过期时间是注解中的属性值seconds。
import com.alibaba.fastjson.JSON;
import com.example.Utils.RedisUtils;
import com.example.annotation.RequestLimit;
import com.example.common.BaseResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;
/**
* 重复请求拦截器
* @author chenliang
*/
@Component
public class RepeatRequestIntercept implements HandlerInterceptor {
@Autowired
private RedisUtils redisUtils;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断请求是否为方法的请求
if (handler instanceof HandlerMethod) {
HandlerMethod hm = (HandlerMethod) handler;
//获取方法中是否有幂等性注解
RequestLimit anno = hm
.getMethodAnnotation(RequestLimit.class);
//若注解为空则直接返回
if (Objects.isNull(anno)) {
return true;
}
long seconds = anno.seconds();
long max = anno.max();
String key = request.getRemoteAddr() + "-" + request
.getMethod() + "-" + request.getRequestURL();
Object requestCountObj = redisUtils.get(key);
if (Objects.isNull(requestCountObj)) {
//若为空则为第一次请求
redisUtils.set(key, 1, seconds);
} else {
//限定时间内的第n次请求
int requestCount = Integer
.parseInt(requestCountObj.toString());
//判断是否超过最大限定请求次数
if (requestCount < max) {
//未超过则请求次数+1
redisUtils.incr(key, 1);
} else {
//否则拒绝请求并返回信息
refuse(response);
return false;
}
}
}
return true;
}
/**
* @param response
* @date 2023-08-10 15:25
* @author Bummon
* @description 拒绝请求并返回结果
*/
private void refuse(HttpServletResponse response) throws IOException {
response.setContentType("application/json;charset=utf-8");
ServletOutputStream os = response.getOutputStream();
BaseResponse<Object> result = BaseResponse.error(100, "请求已提交,请勿重复请求");
String jsonString = JSON.toJSONString(result);
os.write(jsonString.getBytes());
os.flush();
os.close();
}
}
拦截器写好了,但是还得添加注册
5、WebConfig 配置类
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private RepeatRequestIntercept repeatRequestIntercept;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(repeatRequestIntercept);
}
}
6、异常处理器
1)异常标记码
public enum ErrorCode {
SUCCESS(0, "ok", ""),
PARAMS_ERROR(40000, "请求参数错误", ""),
NULL_ERROR(40001, "请求数据为空", ""),
NOT_LOGIN(40100, "未登录", ""),
NO_AUTH(40101, "无权限", ""),
SYSTEM_ERROR(50000, "系统内部异常", ""),
SAVE_ERROR(40011, "用户保存失败", "");
private final int code;
/**
* 状态码信息
*/
private final String message;
/**
* 状态码描述(详情)
*/
private final String description;
ErrorCode(int code, String message, String description) {
this.code = code;
this.message = message;
this.description = description;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
public String getDescription() {
return description;
}
}
1)通用对象返回类
@Data
public class BaseResponse<T> implements Serializable {
private int code;
private T data;
private String message;
private String description;
public BaseResponse(int code, T data, String message, String description) {
this.code = code;
this.data = data;
this.message = message;
this.description = description;
}
public BaseResponse(){}
public static <T> BaseResponse<T> error(int code, String message) {
BaseResponse<T> response = new BaseResponse<>();
response.setCode(code);
response.setMessage(message);
return response;
}
}
7、Redis序列化配置
没有这个配置类会出现格式类型转换异常
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory connectionFactory){
//创建RedisTemplate对象
RedisTemplate<String,Object> template=new RedisTemplate<>();
//设置来凝结工厂
template.setConnectionFactory(connectionFactory);
//创建JSON序列化
GenericJackson2JsonRedisSerializer jsonRedisSerializer=new GenericJackson2JsonRedisSerializer();
//设置key的序列化
template.setKeySerializer(RedisSerializer.string());
template.setHashKeySerializer(RedisSerializer.string());
//设置Value的序列化
template.setValueSerializer(jsonRedisSerializer);
template.setHashValueSerializer(jsonRedisSerializer);
//返回
return template;
}
}
8、测试请求
创建一个Controller类,测试请求
注解中的值可以进行设置
@RequestLimit
public String login(){
return "登录成功!!!";
}
@RequestMapping("/test02")
@RequestLimit(max = 2,seconds = 10)
public String test02(){
return "欢迎进入!!!";
}
测试结果:
请求超过2次后,出现异常提示
限制接口请求次数还可以通过网关来实现,感兴趣的小伙伴可以自行动手实践一下
我的分享结束,如果我的文章对您有所帮助,请点赞、关注一下吧,谢谢!