微服务系统被恶意攻击做的请求限流处理

1.起因

项目是微服务开发的,在正式项目的上线中遭遇了其他服务的恶意攻击。参考了网上的资料。大部分都是在gateway做了请求限流。我的方法也是一样的。

参考链接:https://www.jianshu.com/p/2ba07a16efa8?utm_campaign=hugo

2.准备阶段

1.拥有gateway的微服务

2.拥有redis服务

3.执行流程

3.1 pom引入

 

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>
    <groupId>com.guo.springcloud</groupId>
    <artifactId>sc-gateway-rate-limiter</artifactId>
    <version>1.0.0.RELEASE</version>

    <properties>
        <maven.compiler.target>1.8</maven.compiler.target>
        <maven.compiler.source>1.8</maven.compiler.source>
        <spring.boot.version>2.2.4.RELEASE</spring.boot.version>
        <spring.cloud.version>Hoxton.SR1</spring.cloud.version>
        <spring.cloud.alibaba.version>2.2.0.RELEASE</spring.cloud.alibaba.version>
    </properties>

    <!--
        引入 Spring Boot、Spring Cloud、Spring Cloud Alibaba 三者 BOM 文件,进行依赖版本的管理,防止不兼容。
        在 https://dwz.cn/mcLIfNKt 文章中,Spring Cloud Alibaba 开发团队推荐了三者的依赖关系
     -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>${spring.boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring.cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring.cloud.alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

        </dependencies>
    </dependencyManagement>

    <dependencies>
        <!-- 引入 Spring Cloud Gateway 相关依赖,使用它作为网关,并实现对其的自动配置 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

        <!-- 实现对 Spring Data Redis 的自动化配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>



    </dependencies>

</project>

记得不要引入springboot的依赖。会发生冲突

配置yml文件

server:
  port: 8094

spring:
  application:
    name: sc-gateway-application

  cloud:
    ## Spring Cloud Gateway 配置项,对应 GatewayProperties 类
    gateway:
      # 路由配置项,对应 RouteDefinition 数组
      routes:
        - id: jiansu # 路由的编号
          uri: https://www.jianshu.com/u/ea0462d5074c # 路由到的目标地址(上面作者的简书主页地址)
          predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
            - Path=/jianshu/*         #请求的路径    
          filters:
            - StripPrefix=1
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 1 # 令牌桶的每秒放的数量
                redis-rate-limiter.burstCapacity: 2 # 令牌桶的最大令牌数
                key-resolver: "#{@ipKeyResolver}" # 获取限流 KEY 的 Bean 的名字
        - id: guo # 路由的编号
          uri: https://www.126.com # 路由的目标地址
          predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
            - Path=/*
          filters: # 过滤器,对请求进行拦截,实现自定义的功能,对应 FilterDefinition 数组
            - StripPrefix=1

  ##### Redis 配置项 #####
  redis:
    host: 192.168.0.31
    port: 6379

编写获取ip的工具类

WebFluxUtil
package com.erbadagang.springcloud.gateway.config;

import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Optional;

public class WebFluxUtil {
    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(WebFluxUtil.class);


    private static final String IP_UNKNOWN = "unknown";
    private static final String IP_LOCAL = "127.0.0.1";
    private static final String IPV6_LOCAL = "0:0:0:0:0:0:0:1";
    private static final int IP_LEN = 15;
    /**
     * 获取用户真实IP地址,不直接使用request.getRemoteAddr();的原因是有可能用户使用了代理软件方式避免真实IP地址,
     *
     * 可是,如果通过了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP值,究竟哪个才是真正的用户端的真实IP呢?
     * 答案是取X-Forwarded-For中第一个非unknown的有效IP字符串。
     *
     * 如:X-Forwarded-For:192.168.1.110, 192.168.1.120, 192.168.1.130,
     * 192.168.1.100
     *
     * 用户真实IP为: 192.168.1.110
     *
     * @param request
     * @return
     */
    public static String getIpAddress(ServerHttpRequest request) {
        HttpHeaders headers = request.getHeaders();
        String ipAddress = headers.getFirst("x-forwarded-for");
        if (ipAddress == null || ipAddress.length() == 0 || IP_UNKNOWN.equalsIgnoreCase(ipAddress)) {
            ipAddress = headers.getFirst("Proxy-Client-IP");
        }
        if (ipAddress == null || ipAddress.length() == 0 || IP_UNKNOWN.equalsIgnoreCase(ipAddress)) {
            ipAddress = headers.getFirst("WL-Proxy-Client-IP");
        }
        if (ipAddress == null || ipAddress.length() == 0 || IP_UNKNOWN.equalsIgnoreCase(ipAddress)) {
            ipAddress = Optional.ofNullable(request.getRemoteAddress())
                    .map(address -> address.getAddress().getHostAddress())
                    .orElse("");
            if (IP_LOCAL.equals(ipAddress)|| IPV6_LOCAL.equals(ipAddress)) {
                // 根据网卡取本机配置的IP
                try {
                    InetAddress inet = InetAddress.getLocalHost();
                    ipAddress = inet.getHostAddress();
                } catch (UnknownHostException e) {
                    log.error(e.getMessage());
                }
            }
        }
 
        // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
        if (ipAddress != null && ipAddress.length() > IP_LEN) {
            int index = ipAddress.indexOf(",");
            if (index > 0) {
                ipAddress = ipAddress.substring(0, index);
            }
        }
        return ipAddress;
    }
}

编写限流配置

GatewayConfig
package com.erbadagang.springcloud.gateway.config;

import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
* @description 限流 KEY 的 Bean ,通过解析请求的来源 IP 作为限流 KEY,这样我们就能实现基于 IP 的请求限流。
* @ClassName: GatewayConfig
* @author: 郭秀志 jbcode@126.com
* @date: 2020/7/29 15:26
* @Copyright:
*/
@Configuration
public class GatewayConfig {
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(GatewayConfig.class);
@Bean
public KeyResolver ipKeyResolver() {
return new KeyResolver() {

@Override
public Mono<String> resolve(ServerWebExchange exchange) {
String hostName = exchange.getRequest().getRemoteAddress().getHostName();
String ipAddress = WebFluxUtil.getIpAddress(exchange.getRequest());

log.info("hostName:{},ipAddress:{}",hostName,ipAddress);
// 获取请求的 IP
return Mono.just(ipAddress);
}

};
}

}

定义filter过滤器

package com.inno.leyin.config;

import com.alibaba.fastjson.JSONObject;
import com.inno.leyin.common.constant.CommonConstant;
import com.inno.leyin.common.enums.ResponseMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.factory.RequestRateLimiterGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.ratelimit.RateLimiter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.Disposable;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicBoolean;

@Configuration
public class FilterConfig
{
    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(FilterConfig.class);


    @Bean
    @Order(-1)
    public GlobalFilter ipFilter()
    {
        return new IpFilter();
    }


    @Autowired
    RequestRateLimiterGatewayFilterFactory requestRateLimiterGatewayFilterFactory;
    @Autowired
    RateLimiter rateLimiter;
    @Autowired
    StringRedisTemplate stringRedisTemplate;


    public class IpFilter implements GlobalFilter, Ordered
    {
            //只要是内网192,172的都过,其他的查询出来是外网。判断是否在白名单,不在打死。在黑名单中结束

        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
        {


            log.info("IpFilter前置逻辑");
            ServerHttpRequest request = exchange.getRequest();
            String ipAddress = WebFluxUtil.getIpAddress(request);
            Boolean hasKey = stringRedisTemplate.opsForHash().hasKey("IP_BLACK_LIST", ipAddress);
            if(hasKey){
                ServerHttpResponse response = exchange.getResponse();
                DataBuffer buffer = sendFailInfo(response);
                return response.writeWith(Mono.just(buffer));
            }
            Mono<RateLimiter.Response> erbadagang_rate_limiter = rateLimiter.isAllowed("jiansu", ipAddress);
            AtomicBoolean flag= new AtomicBoolean(false);
            Disposable subscribe = erbadagang_rate_limiter.doOnSuccess(d -> {
                boolean allowed = d.isAllowed();

                System.out.println(allowed);
                log.info("allowed:{},ipAddress:{}", allowed,ipAddress);
                if(!allowed){//代表超出阈值
                    //redisService.setHash("ip",ipAddress,1);
                    log.info("allowed:{},ipAddress:{}", allowed,ipAddress);
                    stringRedisTemplate.opsForHash().put("IP_BLACK_LIST",ipAddress,"1");
                    log.info("ipAddress:{} success");
                    flag.set(true);

                }
            }).subscribe();
            log.info("flag:{} ",flag);
                if(flag.get()){
                    ServerHttpResponse response = exchange.getResponse();
                    DataBuffer buffer = sendFailInfo(response);
                    return response.writeWith(Mono.just(buffer));
                }

            return chain.filter(exchange);
        }

        //   值越小,优先级越高
//    int HIGHEST_PRECEDENCE = -2147483648;
//    int LOWEST_PRECEDENCE = 2147483647;
        @Override
        public int getOrder()
        {
            return HIGHEST_PRECEDENCE + 100;
        }
    }

    private DataBuffer sendFailInfo(ServerHttpResponse response) {
        JSONObject message = new JSONObject();
        message.put("status", ResponseMessage.SENTINEL_ERROR.getCode());
        message.put("data", ResponseMessage.SENTINEL_ERROR.getValue());
        byte[] bits = message.toJSONString().getBytes(StandardCharsets.UTF_8);
        DataBuffer buffer = response.bufferFactory().wrap(bits);
        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        //指定编码,否则在浏览器中会中文乱码
        response.getHeaders().add("Content-Type", "text/plain;charset=UTF-8");
        return buffer;
    }


}

测试:

  1. 启动Redis
  2. 执行 GatewayApplication, 启动网关代码。
  3. 使用浏览器,连续快速访问 http://127.0.0.1:8094/jianshu 地址,将会出现被限流为空白页。

这是一个demo,实际使用的时候需要依赖其他的jar包,所以在使用的时候出现自动配置启动的时候,把该配置移除

建议在生产环境中,

异常问题以及解决方案

在我的系统中开发环境和测试环境都是单机版本,所以不会出现问题,但是在生产环境下出现了问题。 

-- NOSCRIPT No matching script. Please use EVAL.
-- ERR bad lua script for redis cluster, all the keys that the script uses should be passed using the KEYS array, and KEYS should not be in expression
这个原因是阿里云集群环境做了限制。解决方案有两个。

1.https://blog.csdn.net/LHQChocolate/article/details/108668052

2.https://www.jianshu.com/p/6bd82d96ffcf

建议使用第一个

 

posted @ 2022-02-09 13:57  小陈子博客  阅读(427)  评论(0编辑  收藏  举报