#Redis+AOP注解实现IP在单位时间内请求限流

2020-09-27  本文已影响0人  努力耕耘少问收获

现在好多商城类项目对接口限流做了限制,一来为了更好的营造一个健康的绿色的网站,而来防止对网站进行破坏比如黑客写个脚本不停的对服务器进行请求。
话不多说本需求的原来是:#####对每个访问的请求,使用 前缀+请求地址+ip+系统时间,存入 redis 里面,然后 访问之前先去 进行 泛读取key,就是省去最后的系统时间+"*" 进行匹配,看 key 有多少个,如果超过了,注解限制的,就直接返回 请求失败!这里之所以要加上 系统时间,是因为 如果第一次存1,第二次+1 的话可能会 大量的 修改操作,为了方便,所以这样写的#####

①pom.xml配置文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.hytc.mall</groupId>
    <artifactId>hytcmall</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>hytcmall</name>
    <description>弘毅天承商城</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- 动态数据源 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
            <version>2.5.4</version>
        </dependency>

        <!-- mybatisPlus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.1.2</version>
        </dependency>

        <!-- mybatis MVC -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus</artifactId>
            <version>3.3.2</version>
        </dependency>

        <!-- mybatisPlus 代码生成-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.3.2</version>
        </dependency>

        <!--执行main方法前请注意,在生成文件的时候需要有一个模板引擎的选择,MyBatis Plus的默认模板引擎是velocity。我们可以使用freemarker-->
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
        </dependency>

        <!--数据库链接    -->
        <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.19</version>
        </dependency>

        <!-- 非关系型数据库Redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.2.0</version>
        </dependency>

        <!-- 阿里巴巴连接池 -->
        <!-- https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-starter -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.22</version>
        </dependency>
        <!-- 阿里巴巴fastjson序列化 -->
        <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.58</version>
        </dependency>


        <!-- JWT -->
        <!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.10.2</version>
        </dependency>

        <!-- 常用工具类 -->
        <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.10</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/cn.hutool/hutool-all -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.3.0</version>
        </dependency>

        <!--工具类-->
        <dependency>
            <groupId>com.xiaoleilu</groupId>
            <artifactId>hutool-all</artifactId>
            <version>3.3.0</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.8</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>

        <!-- swagger2 -->
        <!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger2 -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <!--防止进入swagger页面报类型转换错误,排除2.9.2中的引用,手动增加1.5.21版本-->
        <dependency>
            <groupId>io.swagger</groupId>
            <artifactId>swagger-annotations</artifactId>
            <version>1.5.21</version>
        </dependency>

        <dependency>
            <groupId>io.swagger</groupId>
            <artifactId>swagger-models</artifactId>
            <version>1.5.21</version>
        </dependency>


        <!-- 这里使用 swagger-bootstrap-ui 替代了原有丑陋的ui,拯救处女座~ -->
        <!-- https://mvnrepository.com/artifact/com.github.xiaoymin/swagger-bootstrap-ui -->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>swagger-bootstrap-ui</artifactId>
            <version>1.9.6</version>
        </dependency>


        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi</artifactId>
            <version>3.9</version>
        </dependency>

        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
            <version>3.7</version>
        </dependency>

        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml-schemas</artifactId>
            <version>3.9</version>
        </dependency>



        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

②application.yml

server:
  port: 90
spring:
  application:
    name: hytc-crm-server
  #缓存配置
  cache:
    type: redis


  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    dynamic:
      #设置默认的数据源或者数据源组,默认值即为master
      primary: master
      datasource:
        #主库配置
        master:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://localhost:3306/hytcmall_20200923?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=false
          username: root
          password: root

    druid:
      connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=50000
      filters: stat,slf4j
      initial-size: 10
      maxActive: 200
      maxPoolPreparedStatementPerConnectionSize: 200
      maxWait: 60000
      min-idle: 5
      minEvictableIdleTimeMillis: 180000
      poolPreparedStatements: true
      stat-view-servlet:
        allow: 127.0.0.1,192.168.163.1
        deny: 192.168.1.73
        #Druid 管理密码
        login-password: 123456
        #Druid 管理账号
        login-username: admin
        reset-enable: false
        url-pattern: /druid/*
      testOnBorrow: false
      testOnReturn: false
      testWhileIdle: true
      #配置一个连接在池中最小生存的时间,单位是毫秒
      timeBetweenEvictionRunsMillis: 300000
      validationQuery: SELECT 1 FROM DUAL
      web-stat-filter:
        enabled: true
        exclusions: '*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*'
        url-pattern: /*
      remove-abandoned: true
      remove-abandoned-timeout-millis: 300
      log-abandoned: true

  redis:
    # Redis数据库索引(默认为0)
    database: 15
    # Redis服务器地址
    host: 127.0.0.1
    # Redis服务器连接端口
    port: 6379
    # Redis服务器连接密码(默认为空)
    password:
    # 连接池最大连接数(使用负值表示没有限制)
    jedis:
      pool:
        ## 连接池最大连接数(使用负值表示没有限制)
        max-active: 20
        # 连接池最大阻塞等待时间(使用负值表示没有限制
        max-wait: -1
        # 连接池中的最大空闲连接
        max-idle: 10
        # 连接池中的最小空闲连接
        min-idle: 0
    # 连接超时时间(毫秒)
    timeout: 5000

#mybatis
mybatis:
  mapper-locations: classpath:/mapper/*.xml
  type-aliases-package: com.hytc.admin.vo
  configuration:
    map-underscore-to-camel-case: true
    #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

#mybatisPlus
mybatis-plus:
  configuration:
    # 原生配置
    cache-enabled: false
    call-setters-on-nulls: true
    map-underscore-to-camel-case: true
  global-config:
    db-config:
      column-underline: true
      #字段策略 IGNORED:"忽略判断",NOT_NULL:"非 NULL 判断"),NOT_EMPTY:"非空判断"
      field-strategy: not_empty
      #数据库相关配置
      #主键类型 AUTO:"数据库ID自增", INPUT:"用户输入ID",ID_WORKER:"全局唯一ID (数字类型唯一ID)", UUID:"全局唯一ID UUID";
      id-type: AUTO
      #数据库大写下划线转换
      #capital-mode: true
      #逻辑删除配置
      logic-delete-value: 0
      logic-not-delete-value: 1

      #自定义填充策略接口实现
      #metaObjectHandler: com.hytc.admin.config.MetaObjectHandlerConfig
      #自定义SQL注入器
      #sql-injector: com.baomidou.springboot.xxx
      configuration:
        map-underscore-to-camel-case: true
        cache-enabled: false

    #刷新mapper 调试神器
    refresh: true
  #mybatis plus mapper文件路径
  mapperLocations: classpath:/mapper/*.xml
  #mybaits plus 实体类路径
  typeAliasesPackage: com.hytc.admin.vo
  typeEnumsPackage: ''

#  logging:
#    config: classpath:log/logback.xml
#    #com.hytc.mapper 该包打印DEBUG级别日志
#    level:
#      com:
#        hytc:
#          mapper: debug
#    path: E:hytcbusslog


logging:
  level:
    com.hytc.mall.hytcmall.mapper: debug

③自定义注解

package com.hytc.mall.hytcmall.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;

/**
 * 限制每个ip对每个方法的访问限制,加上时间限制
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequertIpLimit {

    /**
     * 时间类型,默认毫秒
     * @return
     */
    TimeUnit timeUnit() default TimeUnit.MILLISECONDS ;

    /**
     * 多长时间内限制,默认 60
     * @return
     */
    long t () default 60;

    /**
     * 单位时间内能访问多少次,默认10次
     * @return
     */
    int count () default 10;

}

④aop切面

package com.hytc.mall.hytcmall.aspect;

import com.hytc.mall.hytcmall.annotation.RequertIpLimit;
import com.hytc.mall.hytcmall.cache.RequestIpLImitCache;
import com.hytc.mall.hytcmall.exception.RequestLimitException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**
 * @ClassName: RequestIpLimitAspect
 * @Description:https://blog.csdn.net/yali_aini/article/details/92653982
 * https://juejin.im/post/6844903985388716045
 * http://www.coder55.com/article/76465
 * @Author: BYP <502955177@qq.com>
 * @Date: 2020/9/25 17:37
 * @Copyright: 2019 www.tydic.com Inc. All rights reserved.
 * 注意:本内容仅限于弘毅天承信息技术股份有限公司内部传阅,禁止外泄以及用于其他的商业目
 */

@Aspect//首先,这个@Aspect注释告诉Spring这是个切面类
@Component//然后@Compoment将转换成Spring容器中的bean或者是代理bean
@Order(1)
@Slf4j
public class RequestIpLimitAspect {

    @Autowired
    private RequestIpLImitCache requestIpLImitCache;


    @Pointcut(value = "@annotation(com.hytc.mall.hytcmall.annotation.RequertIpLimit)" )
    public void requestLimitPointCut(){};

    @Around(value = "requestLimitPointCut())")
    public Object requestLimitAround(ProceedingJoinPoint pjp) {

        // 获取 request , 然后获取访问 ip
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();//这个RequestContextHolder是Springmvc提供来获得请求的东西
        HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
        String ip = request.getRemoteAddr();
        String Ip = RequestIpLimitAspect.getRequestIp(request);
        if(StringUtils.isEmpty(Ip)){
            throw new RequestLimitException("300","非法访问!ip不能为空");
        }
        log.info("访问的ip地址为:{}", ip);
        MethodSignature methodSignature = (MethodSignature)pjp.getSignature();
        RequertIpLimit limit = methodSignature.getMethod().getAnnotation(RequertIpLimit.class);
        // 限制访问次数
        int count = limit.count();

        RequestMapping methodAnnotation = methodSignature.getMethod().getAnnotation(RequestMapping.class);
        String url = methodAnnotation.value()[0];
        String key = "_" + url + "_" + Ip +"_";
        if(requestIpLImitCache.count(key) > count ){
            log.info("当前请求次数为:{},该次请求已经超过了规定时间范围内请求的最大次数", requestIpLImitCache.count(key));
            throw new RequestLimitException("300","访问失败!超过访问限制!");
        }
        // 将访问存进缓存
        requestIpLImitCache.add(key+System.currentTimeMillis(), "1", limit.timeUnit(), limit.t());
        //记录开始时间
        long startTime = System.currentTimeMillis();
        //获取传入目标方法的参数
        Object[] args = pjp.getArgs();
        Object result = null;
        try {
            // 执行访问并返回数据
            result = pjp.proceed(args);
            return result;
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            return null;
        } finally {
            log.info("当前请求的Key是:{}", key);
            //记录结束时间
            long endTime = System.currentTimeMillis();
            log.info("请求耗时为:{}", (endTime - startTime));
        }
    }




    public static String getRequestIp(HttpServletRequest request){
        // 获取请求IP
        String ip = request.getHeader("x-forwarded-for");
        if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip) || "null".equals(ip)){
            ip = "" + request.getHeader("Proxy-Client-IP");
        }
        if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip) || "null".equals(ip)){
            ip = "" + request.getHeader("WL-Proxy-Client-IP");
        }
        if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip) || "null".equals(ip)){
            ip = "" + request.getRemoteAddr();
        }
        if("0.0.0.0".equals(ip) || "0.0.0.0.0.0.1".equals(ip) || "localhost".equals(ip) || "127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) {
            ip = "127.0.0.1";
        }
        return ip;
    }


}

⑤Redis工具类

package com.hytc.mall.hytcmall.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * @ClassName: RedisConfig
 * @Description:https://www.cnblogs.com/lzhdonald/p/11560002.html
 * @Author: BYP <502955177@qq.com>
 * @Date: 2020/9/24 10:40
 * @Copyright: 2019 www.tydic.com Inc. All rights reserved.
 * 注意:本内容仅限于弘毅天承信息技术股份有限公司内部传阅,禁止外泄以及用于其他的商业目
 */

@Configuration
@EnableCaching
public class RedisConfig {

    //配置使用jedis作为redis的客户端
    @Bean
    public JedisConnectionFactory jedisConnectionFactory(){

        return new JedisConnectionFactory();
    }

    @Bean
    public RedisTemplate<Object,Object> redisTemplate(){
        // 创建RedisTemplate<String, Object>对象
        RedisTemplate<Object, Object> template = new RedisTemplate<Object, Object>();
        // 配置连接工厂
        template.setConnectionFactory(jedisConnectionFactory());

        // 定义Jackson2JsonRedisSerializer序列化对象
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        // 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        // 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会报异常
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        StringRedisSerializer stringSerial = new StringRedisSerializer();
        // redis key 序列化方式使用stringSerial
        template.setKeySerializer(stringSerial);
        // redis value 序列化方式使用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // redis hash key 序列化方式使用stringSerial
        template.setHashKeySerializer(stringSerial);
        // redis hash value 序列化方式使用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();

        return template;

    }
}

package com.hytc.mall.hytcmall.cache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;


/**
 * @ClassName: RequestIpLImitCache
 * @Description:
 * @Author: BYP <502955177@qq.com>
 * @Date: 2020/9/25 17:48
 * @Copyright: 2019 www.tydic.com Inc. All rights reserved.
 * 注意:本内容仅限于弘毅天承信息技术股份有限公司内部传阅,禁止外泄以及用于其他的商业目
 */

@Component
public class RequestIpLImitCache {

    private static final String PREFIX = "REQUEST_IP_LIMIT:";

    @Autowired
    private RedisTemplate redisTemplate;

    public void add(String key , String value , TimeUnit timeUnit , long t){
        redisTemplate.opsForValue().set(PREFIX + key, value, t, timeUnit);
    }

    public int count(String key ){
        return redisTemplate.keys(PREFIX + key + "*").size();
    }

}

⑦spring全局事务GlobalExceptionHandler

package com.hytc.mall.hytcmall.exception;

import com.google.common.collect.Maps;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.servlet.http.HttpServletRequest;
import java.util.Map;

/**
 * @ClassName: GlobalExceptionHandler
 * @Description:全局异常处理
 * @Author: BYP <502955177@qq.com>
 * @Date: 2020/9/27 10:03
 * @Copyright: 2019 www.tydic.com Inc. All rights reserved.
 * 注意:本内容仅限于弘毅天承信息技术股份有限公司内部传阅,禁止外泄以及用于其他的商业目
 */

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(RequestLimitException.class)
    public Map defaultExceptionHandler(HttpServletRequest request,Exception e){
        Map<String,Object>map = Maps.newConcurrentMap();
        if(e instanceof RequestLimitException){
            RequestLimitException limit = (RequestLimitException) e;
            map.put("code", limit.getCode());
            map.put("msg", limit.getMsg());
        }else{
            map.put("code", -1);
            map.put("msg", "系统异常");
        }
        //未知错误
        return map;
    }
}

⑧请求限制异常

package com.hytc.mall.hytcmall.exception;

import lombok.Data;

/**
 * @ClassName: RequestLimit
 * @Description:
 * @Author: BYP <502955177@qq.com>
 * @Date: 2020/9/27 9:59
 * @Copyright: 2019 www.tydic.com Inc. All rights reserved.
 * 注意:本内容仅限于弘毅天承信息技术股份有限公司内部传阅,禁止外泄以及用于其他的商业目
 */

@Data
public class RequestLimitException extends RuntimeException{

    /*错误码*/
    private String code;

    /*错误提示*/
    private String msg;

    public RequestLimitException(){

    }

    public RequestLimitException(String code,String msg){
        this.code = code;
        this.msg = msg;

    }
}

上一篇下一篇

猜你喜欢

热点阅读