【接口防刷机制】
主要防止短时间接口被大量调用(攻击),出现系统崩溃和系统爬虫问题,提升服务的可用性。限制同一用户一定时间内(如1 min)只能访问固定次数,可以减少对业务的侵入,在服务端对系统做一层保护.
【实现方案】
本文主要是通过 自定义注解+redis+spring aop+全局异常的方式实现接口限流防刷功能。
- 自定义注解
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface RequestLimit {
/**
* 调用的次数
* @return
*/
int count() default 3;
/**
* 时间段; 在time内调用的次数count -单位秒
* @return
*/
int frameTime() default 1;
/**
* 锁定时间 -单位小时
* @return
*/
int lockTime() default 1;
}
- spring AOP切面
@Slf4j
@Aspect
@Component
public class RequestLimitContract {
@Autowired
private TblUserService userService;
@Autowired
private TblBlackListService tblBlackListService;
@Autowired
private RedisClient redisClient;
//region 环绕通知
@Around("@annotation(limit)")
@Transactional(rollbackFor = Exception.class)
public Object requestLimit(ProceedingJoinPoint process, RequestLimit limit) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
long startTime = System.currentTimeMillis();
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
MethodSignature methodSignature = (MethodSignature) process.getSignature();
Method method = methodSignature.getMethod();
String methodName = method.getDeclaringClass().getName() + "." + method.getName();
// 根据 IP + API 限流
String requestURI = request.getRequestURI();
String className = method.getDeclaringClass().getName();
String uid = request.getHeader("uid");
Object[] args = process.getArgs();
if (StringUtils.isEmpty(uid)) {
uid = getUser (args[0], requestURI);
}
log.info("请求时间:{}, clientIp:{}, 请求方法:{}, 请求参数{}", simpleDateFormat.format(startTime), "", methodName);
// result是方法的最终返回结果
Object result = null;
try {
// 查询ip是否被锁定 -是 直接返回 -否 继续流程
String ipAddress = getIpAddr(request);
RedisVo redisVo = new RedisVo();
redisVo.setKey(ipAddress);
GenericResponse getTime = redisClient.getExpire(redisVo);
log.error("-------------redis过期时间-------------" + getTime);
if (getTime.getCode().equals(CommonEnum.ResponseCode.Success.getCode())) {
// 获取key的过期时间
String time = getTime.getResult().toString();
long intValue = Double.valueOf(time).longValue();
if (intValue >= limit.frameTime()) {
return new GenericResponse(CommonEnum.ResponseCode.操作过于频繁.getCode(), "已经放入小黑屋,封锁一个小时");
}
}
// TODO 检查用户id (或者ip)是否在黑名单中
boolean b = checkBlackList(uid);
if (b) {
return new GenericResponse(CommonEnum.ResponseCode.操作过于频繁.getCode(), "用户操作异常");
}
redisVo.setKey(requestURI + ipAddress);
GenericResponse getRedisCodeResponse = redisClient.get(redisVo);
Integer maxTimes = 0;
if (StringUtils.isNotEmpty(getRedisCodeResponse.getResult().toString())) {
maxTimes = Integer.parseInt(getRedisCodeResponse.getResult().toString());
}
if (maxTimes.equals(0)) {
//set时一定要加过期时间
redisVo.setTime(limit.frameTime());
redisVo.setValue("1");
redisVo.setTimeUnit(TimeUnit.SECONDS);
redisClient.setByTime(redisVo);
log.error("-------------用户正常-------------");
//调用执行目标方法
result = process.proceed();
} else if (maxTimes < limit.count()) {
redisVo.setValue(String.valueOf(maxTimes + 1));
redisClient.incr(redisVo);
log.error("------------请求频繁--------------");
//调用执行目标方法
result = process.proceed();
} else {
// TODO 将该ip对应用户UID 设备号插入黑名单中,并将上下级用户插入观察者表中
if (!StringUtils.isEmpty(uid)&&!"0".equals(uid.trim())) {
tblBlackListService.insertBlack(uid, ipAddress, requestURI);
}
// 访问太频繁,加入到redis中锁定一个小时
log.error("------------ 访问太频繁,ip加入到redis中锁定一个小时--------------");
redisVo.setKey(ipAddress);
redisVo.setTime(limit.lockTime());
redisVo.setValue("1");
redisVo.setTimeUnit(TimeUnit.HOURS);
redisClient.setByTime(redisVo);
return new GenericResponse(CommonEnum.ResponseCode.操作过于频繁.getCode(), "操作过于频繁");
}
} catch (Throwable throwable) {
String exception = throwable.getClass() + ":" + throwable.getMessage();
long costTime = System.currentTimeMillis() - startTime;
log.error("请求时间:{}, 请求耗时:{}, 请求类名:{}, 请求方法:{}, 请求参数:{}, 请求结果:{}", startTime, costTime, className, methodName, "", exception);
return new GenericResponse(CommonEnum.ResponseCode.Fail.getCode(), "服务器异常", exception);
}
long costTime = System.currentTimeMillis() - startTime;
log.info("请求时间:{}, 请求耗时:{}, 请求类名:{}, 请求方法:{}, 请求参数:{}, 请求结果:{}", simpleDateFormat.format(startTime), costTime, className, methodName, "", new Gson().toJson(result));
return result;
}
// 查询用户是否在黑名单中
private boolean checkBlackList(String uid) {
TblBlackList tblBlackList = new TblBlackList();
tblBlackList.setUid(uid);
tblBlackList = tblBlackListService.getTblBlackListBy(tblBlackList);
if (tblBlackList != null) {
return true;
} else {
return false;
}
}
//获取用户id
private String getUser (Object result, String methodName) {
if (methodName.equals("/user/sendLoginVerificationCode")) {
// 请求参数中包含手机号
String userTelephone = (String) JSONObject.fromObject(result).get("telephone");
TblUser tblUser = userService.getEntityByPhoneTest(userTelephone);
if (tblUser != null) {
return tblUser.getId().toString();
}
} else if (methodName.equals("/task/invite/bindPhone")) {
String uid = (JSONObject.fromObject(result).get("uid")).toString();
return uid;
} else if (methodName.equals("/sign/day/sinIn")) {
String uid = (JSONObject.fromObject(result).get("uid")).toString();
return uid;
} else {
String uid = (JSONObject.fromObject(result).get("uid")).toString();
return uid;
}
return "";
}
}
只需要限流的接口上加上注解即可:
@RequestLimit(count = 3, frameTime = 60, lockTime = 1)
【流程图】