先上代码,使用自定义 @AccessLimit(seconds = 30,maxCount = 10)注解可以实现对于一个用户(IP或者账号),30秒内被注解的方法只能被访问10次,30秒后又重置次数。
@RequestMapping("/redis")
@AccessLimit(seconds = 30,maxCount = 10)
@Cacheable(cacheNames = "user", key = "#a+''+#b", unless = "#a==null || # b==null")
public String redis(String a, String b) {
RLock lock = redissonClient.getLock("lock");
lock.lock();
System.out.println(Thread.currentThread().getName()+"拿到锁对象。");
String o;
try {
o = (String) redisTemplate.opsForValue().get("user::" + a + b);
if (o==null){
System.out.println("查询数据库");
o="数据库数据"+a+b;
}
}finally {
if (lock!=null){
lock.unlock();
}
}
return o;
}
其中我这里使用了spring cache的注解@Cacheable,在没有使用限流注解之前,每次将数据库返回的数据存入redis,但是问题就来了,因为存入redis的key是根据搜索条件a和b变化的,如果每次修改a和b的值,或者用时间戳去作为参数,那么每次都会进入方法体,去查询数据库,最终导致redis缓存穿透,所以在这里自定义了@AccessLimit注解,来防止恶意刷接口。
第一步,在maven中引入aop依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
第二步,编写限流AccessLimit注解。
package com.salong.myself.config.AccessLimit;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Salong
* @date 2021/5/17 16:32
* @Email:salong0503@aliyun.com
* 限流(防止接口被刷)
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AccessLimit {
int seconds() default 60;
int maxCount() default 10;
}
第三步,将AccessLimit注解放入AOP切点中。
package com.salong.myself.config.AccessLimit;
import com.alibaba.fastjson.JSONObject;
import com.salong.myself.common.response.Response;
import com.salong.myself.common.response.RetCode;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
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.net.InetAddress;
import java.net.UnknownHostException;
import java.util.concurrent.TimeUnit;
/**
* @author Salong
* @date 2021/5/17 16:37
* @Email:salong0503@aliyun.com
*/
@Aspect
@Component
@Slf4j
public class AccessLimitAop {
@Resource
private RedisTemplate<String,Integer> redisTemplate;
/**
* 切入点为AccessLimit注解
*/
@Pointcut("@annotation(com.salong.myself.config.AccessLimit.AccessLimit)")
public void cutLimit(){}
@Around("cutLimit() && @annotation(accessLimit)")
public Object around(ProceedingJoinPoint point,AccessLimit accessLimit) throws Throwable {
MethodSignature ms = (MethodSignature) point.getSignature();
Method method = ms.getMethod();
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
//设置redis的key,用方法名和ip来命名
String ip=getIpAddrFromNginx(request);
String key=ip+"::"+ms.getName()+method.getName();
Integer count = redisTemplate.opsForValue().get(key);
if (null == count || -1 == count) {
redisTemplate.opsForValue().set(key, 1,accessLimit.seconds(), TimeUnit.SECONDS);
return point.proceed();
}
//判断是否限流
if (count < accessLimit.maxCount()){
redisTemplate.opsForValue().increment(key);
return point.proceed();
}else {
log.warn("开始限流");
//返回封装的Response类
return JSONObject.toJSONString(Response.error(RetCode.REQUEST_FREQUENTLY));
}
}
/**
* 从nginx获取到用户ip,防止伪装ip
*
* @param request
* @return
* @throws UnknownHostException
*/
public static String getIpAddrFromNginx(HttpServletRequest request) throws UnknownHostException {
// 从Nginx中X-Real-IP获取真实ip
String ipAddress = request.getHeader("X-Real-IP");
if (ipAddress != null && ipAddress.length() > 0 && !"unknown".equalsIgnoreCase(ipAddress)) {
log.info("从X-Real-IP中获取到ip:" + ipAddress);
return ipAddress;
}
//从Nginx中x-forwarded-for获取真实ip
ipAddress = request.getHeader("x-forwarded-for");
if (ipAddress != null && ipAddress.length() > 0 && !"unknown".equalsIgnoreCase(ipAddress)) {
// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
int index = ipAddress.indexOf(",");
if (index > 0) {
ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
}
log.info("从x-forwarded-for中获取到ip:" + ipAddress);
return ipAddress;
}
ipAddress = request.getRemoteAddr();
if ("127.0.0.1".equals(ipAddress) || "0:0:0:0:0:0:0:1".equals(ipAddress)) {
// 根据网卡取本机配置的IP
ipAddress = InetAddress.getLocalHost().getHostAddress();
}
log.info("从request.getRemoteAddr()中获取到ip:" + ipAddress);
return ipAddress;
}
}
注意:这个注解只能限制请求进入方法体内的频率,如果使用了Cacheable类似的注解,那么如果redis有匹配的值,将不会再进入方法体,直接查询redis并返回,所以不能够防止恶意刷查询redis,如果要防止恶意刷查询redis,那么需要将查询redis的操作写入方法体。