手写基于AOP限流的注解,防止恶意刷接口

先上代码,使用自定义 @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的操作写入方法体。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

却诚Salong

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值