|
|
|
|
@ -1,7 +1,6 @@
|
|
|
|
|
package com.aurora.interceptor;
|
|
|
|
|
|
|
|
|
|
import com.alibaba.fastjson.JSON;
|
|
|
|
|
|
|
|
|
|
import com.aurora.annotation.AccessLimit;
|
|
|
|
|
import com.aurora.model.vo.ResultVO;
|
|
|
|
|
import com.aurora.service.RedisService;
|
|
|
|
|
@ -20,47 +19,106 @@ import java.nio.charset.StandardCharsets;
|
|
|
|
|
|
|
|
|
|
import static com.aurora.constant.CommonConstant.APPLICATION_JSON;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 自定义的访问限制拦截器,实现 HandlerInterceptor 接口,
|
|
|
|
|
* 用于限制用户在指定时间窗口内对某个接口的访问次数,防止恶意刷接口或接口滥用。
|
|
|
|
|
*/
|
|
|
|
|
@Log4j2
|
|
|
|
|
@Component
|
|
|
|
|
@SuppressWarnings("all")
|
|
|
|
|
public class AccessLimitInterceptor implements HandlerInterceptor {
|
|
|
|
|
|
|
|
|
|
// 自动注入 RedisService,用于与 Redis 进行交互,实现访问次数的计数和过期时间设置
|
|
|
|
|
@Autowired
|
|
|
|
|
private RedisService redisService;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 在请求处理之前执行的方法,用于拦截和处理请求。
|
|
|
|
|
* 该方法会在请求到达 Controller 方法之前被调用,用于执行前置处理逻辑。
|
|
|
|
|
*
|
|
|
|
|
* @param httpServletRequest 当前的 HTTP 请求对象,包含请求的详细信息
|
|
|
|
|
* @param httpServletResponse 当前的 HTTP 响应对象,用于设置响应内容和状态码
|
|
|
|
|
* @param handler 当前请求的处理对象,通常是 HandlerMethod 类型,表示一个 Controller 方法
|
|
|
|
|
* @return 如果返回 true,表示继续处理请求;如果返回 false,表示中断请求处理链
|
|
|
|
|
* @throws Exception 如果在处理过程中发生异常
|
|
|
|
|
*/
|
|
|
|
|
@Override
|
|
|
|
|
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object handler) throws Exception {
|
|
|
|
|
// 检查当前的 handler 是否是 HandlerMethod 类型,即是否对应一个 Controller 方法
|
|
|
|
|
if (handler instanceof HandlerMethod) {
|
|
|
|
|
// 将 handler 强制转换为 HandlerMethod 类型,以便获取方法级别的注解
|
|
|
|
|
HandlerMethod handlerMethod = (HandlerMethod) handler;
|
|
|
|
|
|
|
|
|
|
// 获取当前方法上的 @AccessLimit 注解,用于获取访问限制的配置(如时间窗口、最大访问次数)
|
|
|
|
|
AccessLimit accessLimit = handlerMethod.getMethodAnnotation(AccessLimit.class);
|
|
|
|
|
|
|
|
|
|
// 如果当前方法没有被 @AccessLimit 注解修饰,则不进行访问限制,直接放行
|
|
|
|
|
if (accessLimit != null) {
|
|
|
|
|
// 从 @AccessLimit 注解中获取时间窗口(秒)和最大访问次数
|
|
|
|
|
long seconds = accessLimit.seconds();
|
|
|
|
|
int maxCount = accessLimit.maxCount();
|
|
|
|
|
|
|
|
|
|
// 生成用于 Redis 计数的唯一键,通常结合客户端 IP 和方法名,确保不同用户和方法有不同的计数
|
|
|
|
|
String key = IpUtil.getIpAddress(httpServletRequest) + "-" + handlerMethod.getMethod().getName();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 使用 RedisService 的 incrExpire 方法,对指定键进行自增操作,并设置过期时间为指定的时间窗口
|
|
|
|
|
// incrExpire 方法返回当前键的计数值
|
|
|
|
|
long q = redisService.incrExpire(key, seconds);
|
|
|
|
|
|
|
|
|
|
// 如果当前计数值超过了最大访问次数,表示请求过于频繁,触发访问限制
|
|
|
|
|
if (q > maxCount) {
|
|
|
|
|
// 调用 render 方法,向客户端返回一个表示访问限制的 JSON 响应
|
|
|
|
|
render(httpServletResponse, ResultVO.fail("请求过于频繁," + seconds + "秒后再试"));
|
|
|
|
|
|
|
|
|
|
// 记录警告日志,提示请求次数超过限制
|
|
|
|
|
log.warn(key + "请求次数超过每" + seconds + "秒" + maxCount + "次");
|
|
|
|
|
|
|
|
|
|
// 返回 false,中断请求处理链,不再继续处理该请求
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 如果当前计数值未超过最大访问次数,允许请求继续处理
|
|
|
|
|
return true;
|
|
|
|
|
} catch (RedisConnectionFailureException e) {
|
|
|
|
|
// 如果在访问 Redis 时发生连接失败异常,记录警告日志
|
|
|
|
|
log.warn("redis错误: " + e.getMessage());
|
|
|
|
|
|
|
|
|
|
// 返回 false,中断请求处理链,不再继续处理该请求
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 如果当前请求没有 @AccessLimit 注解,或者不是 HandlerMethod 类型,直接放行
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 渲染并返回 JSON 格式的响应结果。
|
|
|
|
|
* 该方法将 ResultVO 对象序列化为 JSON 字符串,并写入 HTTP 响应体。
|
|
|
|
|
*
|
|
|
|
|
* @param response 当前 HTTP 响应对象,用于设置响应内容和状态码
|
|
|
|
|
* @param resultVO 包含响应结果的 ResultVO 对象,通常包含状态码、消息和数据
|
|
|
|
|
* @throws Exception 如果在写入响应时发生异常
|
|
|
|
|
*/
|
|
|
|
|
private void render(HttpServletResponse response, ResultVO<?> resultVO) throws Exception {
|
|
|
|
|
// 设置响应的内容类型为 JSON,确保客户端正确解析响应体
|
|
|
|
|
response.setContentType(APPLICATION_JSON);
|
|
|
|
|
|
|
|
|
|
// 获取响应的输出流,用于写入响应数据
|
|
|
|
|
OutputStream out = response.getOutputStream();
|
|
|
|
|
|
|
|
|
|
// 将 ResultVO 对象序列化为 JSON 字符串
|
|
|
|
|
String str = JSON.toJSONString(resultVO);
|
|
|
|
|
|
|
|
|
|
// 将 JSON 字符串写入响应输出流
|
|
|
|
|
out.write(str.getBytes(StandardCharsets.UTF_8));
|
|
|
|
|
|
|
|
|
|
// 刷新输出流,确保数据被发送到客户端
|
|
|
|
|
out.flush();
|
|
|
|
|
|
|
|
|
|
// 关闭输出流,释放资源
|
|
|
|
|
out.close();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|