【接口防刷机制】
主要防止短时间接口被大量调用(攻击),出现系统崩溃和系统爬虫问题,提升服务的可用性。限制同一用户一定时间内(如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 {@Autowiredprivate TblUserService userService;@Autowiredprivate TblBlackListService tblBlackListService;@Autowiredprivate 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;}}//获取用户idprivate 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)
【流程图】