天天看點

springboot接口服務,防刷、防止請求攻擊,AOP實作pom.xml 中加入 AOP 依賴AOP自定義注解類AOP切面業務類測試

本文使用AOP的方式防止spring boot的接口服務被網絡攻擊

  1. pom.xml 中加入 AOP 依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
           
  1. AOP自定義注解類

package org.jeecg.common.aspect.annotation;

import java.lang.annotation.*;

/**
 * 用于防刷限流的注解
 *      預設是5秒内隻能調用一次
 */
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {

    /** 限流的key */
    String key() default "limit:";

    /** 周期,機關是秒 */
    int cycle() default 5;

    /** 請求次數 */
    int count() default 1;

    /** 預設提示資訊 */
    String msg() default "請勿重複點選";
}
           
  1. AOP切面業務類

package org.jeecg.common.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.jeecg.common.aspect.annotation.RateLimit;
import org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint;
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.util.concurrent.TimeUnit;

/**
 * 切面類:實作限流校驗
 */
@Aspect
@Component
public class AccessLimitAspect {

    @Resource
    private RedisTemplate<String, Integer> redisTemplate;

    /**
     * 這裡我們使用注解的形式
     * 當然,我們也可以通過切點表達式直接指定需要攔截的package,需要攔截的class 以及 method
     */
    @Pointcut("@annotation(org.jeecg.common.aspect.annotation.RateLimit)")
    public void limitPointCut() {
    }

    /**
     * 環繞通知
     */
    @Around("limitPointCut()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        // 擷取被注解的方法
        MethodInvocationProceedingJoinPoint mjp = (MethodInvocationProceedingJoinPoint) pjp;
        MethodSignature signature = (MethodSignature) mjp.getSignature();
        Method method = signature.getMethod();

        // 擷取方法上的注解
        RateLimit rateLimit = method.getAnnotation(RateLimit.class);
        if (rateLimit == null) {
            // 如果沒有注解,則繼續調用,不做任何處理
            return pjp.proceed();
        }
        /**
         * 代碼走到這裡,說明有 RateLimit 注解,那麼就需要做限流校驗了
         *  1、這裡可以使用Redis的API做計數校驗
         *  2、這裡也可以使用Lua腳本做計數校驗,都可以
         */
        //擷取request對象
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        // 擷取請求IP位址
        String ip = getIpAddr(request);
        // 請求url路徑
        String uri = request.getRequestURI();
        //存到redis中的key
        String key = "RateLimit:" + ip + ":" + uri;
        // 緩存中存在key,在限定通路周期内已經調用過目前接口
        if (redisTemplate.hasKey(key)) {
            // 通路次數自增1
            redisTemplate.opsForValue().increment(key, 1);
            // 超出通路次數限制
            if (redisTemplate.opsForValue().get(key) > rateLimit.count()) {
                throw new RuntimeException(rateLimit.msg());
            }
            // 未超出通路次數限制,不進行任何操作,傳回true
        } else {
            // 第一次設定資料,過期時間為注解确定的通路周期
            redisTemplate.opsForValue().set(key, 1, rateLimit.cycle(), TimeUnit.SECONDS);
        }
        return pjp.proceed();
    }

    //擷取請求的歸屬IP位址
    private String getIpAddr(HttpServletRequest request) {
        String ipAddress = null;
        try {
            ipAddress = request.getHeader("x-forwarded-for");
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("Proxy-Client-IP");
            }
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("WL-Proxy-Client-IP");
            }
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getRemoteAddr();
            }
            // 對于通過多個代理的情況,第一個IP為用戶端真實IP,多個IP按照','分割
            if (ipAddress != null && ipAddress.length() > 15) {
                // = 15
                if (ipAddress.indexOf(",") > 0) {
                    ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
                }
            }
        } catch (Exception e) {
            ipAddress = "";
        }
        return ipAddress;
    }
}
           
  1. 測試

package org.jeecg.modules.api.controller;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.aspect.annotation.RateLimit;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 測試接口
 * @author wujiangbo
 * @date 2022-08-23 18:50
 */
@RestController
@RequestMapping("/test")
public class TestController {

    //4秒内隻能通路2次
    @RateLimit(key= "testLimit", count = 2, cycle = 4, msg = "大哥、慢點刷請求!")
    @GetMapping("/test001")
    public Result<?> rate() {
        System.out.println("請求成功");
        return Result.OK("請求成功!");
    }
}