Spring Cloud Alibaba-Gateway之路由、限流、熔斷、日志、鑒權(3)


在微服務架構中,網關的職責包括路由、鑒權、限流、日志、監控、灰度發布等,目前主流的方案有Neflix Zuul和Spring Cloud Gateway。

一、路由

2 創建項目

創建一個SpringBoot項目,添加Cloud Gateway和Nacos相關依賴,不要添加Web依賴。完成后的pom如下:

<properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Greenwich.SR1</spring-cloud.version>
        <alibaba.version>0.9.0.RELEASE</alibaba.version>
    </properties>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
 
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>
 
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
            <version>${alibaba.version}</version>
        </dependency>
 
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <version>${alibaba.version}</version>
        </dependency>
 
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
 
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
 
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

application.yml

server:
  port: 8084

bootstrap.yml

spring:
  application:
    name: gateway
  cloud:
    nacos:
      config:
        server-addr: 127.0.0.1:8848
      discovery:
        server-addr: 127.0.0.1:8848
    gateway:
      discovery:
        locator:
          enabled: true
      routes:
      - id: payment-router
        uri: lb://payment-service
        predicates:
        - Path=/pay/**

在上面的配置中:

id: payment-router 值隨意,方便記憶並且在所有路由定義中唯一即可
uri: lb://payment-service lb://為固定寫法,表示開啟負載均衡;payment-service即服務在Nacos中注冊的名字
predicates:- Path=/pay/** 使用"Path Route Predicate Factory",規則為/pay開頭的任意URI

最后一步,在啟動類上增加@EnableDiscoveryClient注解

@SpringBootApplication
@EnableDiscoveryClient
public class GatewayApplication {

除了Path Route Predicate Factory,Gateway還支持多種設置方式:

類型 示例
After After=2017-01-20T17:42:47.789-07:00[America/Denver]
Before Before=2017-01-20T17:42:47.789-07:00[America/Denver]
Between 2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver]
Cookie Cookie=chocolate, ch.p
Header Header=X-Request-Id, \d+
Host Host=**.somehost.org
Method Method=GET
Path Path=/foo/{segment}
Query Query=baz
RemoteAddr RemoteAddr=192.168.1.1/24

 

 

 

 

 

 

 

 

 

 

 

 可參照SpringCloud路由Gateway(5)

二、限流

Gateway通過內置的RequestRateLimiter過濾器實現限流,使用令牌桶算法,借助Redis保存中間數據。用戶可通過自定義KeyResolver設置限流維度,例如:

對請求的目標URL進行限流
對來源IP進行限流
特定用戶進行限流
本例針對來源IP限流。

添加Redis依賴:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>

在application.yml中添加Redis配置:

server:
  port: 8084
spring:
  redis:
    host: 127.0.0.1
    port: 6379

SpringBoot自動配置的RedisTemplate生成的key中會包含特殊字符,所以創建一個RedisTemplate替換

@Configuration
public class RedisConfiguration {
 
    @Bean("redisTemplate")
    public RedisTemplate redisTemplate(@Value("${spring.redis.host}") String host,
                                       @Value("${spring.redis.port}") int port) {
        RedisTemplate redisTemplate = new RedisTemplate();
        RedisSerializer stringRedisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        redisTemplate.setKeySerializer(stringRedisSerializer);
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setConnectionFactory(standaloneConnectionFactory(host, port));
        return redisTemplate;
    }
 
    protected JedisConnectionFactory standaloneConnectionFactory(String host, int port) {
        RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
        redisStandaloneConfiguration.setHostName(host);
        redisStandaloneConfiguration.setPort(port);
        return new JedisConnectionFactory(redisStandaloneConfiguration);
    }
}

自定義KeyResolver

@Configuration
public class RateLimiterConfiguration {
 
    @Bean(value = "ipKeyResolver")
    public KeyResolver ipKeyResolver() {
        return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
    }
}

最后一步,在bootstrap.yml的payment-router路由中加入限流過濾器

...
      routes:
      - id: payment-router
        uri: lb://payment-service
        predicates:
        - Path=/pay/**
        filters:
        - name: RequestRateLimiter
          args:
            redis-rate-limiter.replenishRate: 1
            redis-rate-limiter.burstCapacity: 5
            key-resolver: '#{@ipKeyResolver}'

其中令牌桶容量redis-rate-limiter.burstCapacity設置為5,即1秒內最大請求通行數為5個,令牌桶填充速率redis-rate-limiter.replenishRate設置為1。

三、熔斷

網關是所有請求的入口,如果部分后端服務延時嚴重,則可能導致大量請求堆積在網關上,拖垮網關進而癱瘓整個系統。這就需要對響應慢的服務做超時快速失敗處理,即熔斷。

添加hystrix依賴

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>

在bootstrap.yml中添加默認過濾器

spring:
  ...
  cloud:
    ...
    gateway:
      discovery:
        locator:
          enabled: true
      default-filters:
      - name: Hystrix
        args:
          name : default
          fallbackUri: 'forward:/defaultFallback'
      ...
hystrix:
  command:
    default:
      execution:
        isolation:
          strategy: SEMAPHORE
          thread:
            timeoutInMilliseconds: 2000

創建降級處理FallbackController.java

@RestController
public class FallbackController {
    @RequestMapping("/defaultFallback")
    public Map defaultFallback() {
        Map map = new HashMap<>();
        map.put("code", 1);
        map.put("message", "服務異常");
        return map;
    }
}

在Nacos后台中把payment-service-dev.properties的sleep值修改為2000模擬服務延時效果,然后測試

四、日志

在引入網關后,通常會把每個服務都要做的工作,諸如日志、安全驗證等轉移到網關處理以減少重復開發。

1 加入log4j2

這里使用log4j2作為日志組件,首先添加log4j2的依賴並排除SpringBoot默認日志組件的依賴

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
        </dependency>
 
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

在resources目錄下創建log4j2-spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" monitorInterval="1800">
    <properties>
        <property name="LOG_HOME">D:/Logs/gateway</property>
        <property name="REQUEST_FILE_NAME">request</property>
        <property name="INFO_FILE_NAME">info</property>
    </properties>
 
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" />
        </Console>
 
        <RollingRandomAccessFile name="info-log"
                                 fileName="${LOG_HOME}/${INFO_FILE_NAME}.log"
                                 filePattern="${LOG_HOME}/$${date:yyyy-MM}/${INFO_FILE_NAME}-%d{yyyy-MM-dd}-%i.log">
            <PatternLayout
                    pattern="%date{yyyy-MM-dd HH:mm:ss.SSS} %level [%thread][%file:%line] - %msg%n"/>
            <Policies>
                <TimeBasedTriggeringPolicy/>
                <SizeBasedTriggeringPolicy size="100 MB"/>
            </Policies>
            <DefaultRolloverStrategy max="100"/>
        </RollingRandomAccessFile>
 
        <RollingRandomAccessFile name="request-log"
                                 fileName="${LOG_HOME}/${REQUEST_FILE_NAME}.log"
                                 filePattern="${LOG_HOME}/$${date:yyyy-MM}/${REQUEST_FILE_NAME}-%d{yyyy-MM-dd}-%i.log">
            <PatternLayout
                    pattern="%date{yyyy-MM-dd HH:mm:ss.SSS} %level [%thread][%file:%line] - %msg%n"/>
            <Policies>
                <TimeBasedTriggeringPolicy/>
                <SizeBasedTriggeringPolicy size="200 MB"/>
            </Policies>
            <DefaultRolloverStrategy max="200"/>
        </RollingRandomAccessFile>
    </Appenders>
 
    <Loggers>
        <Root level="info">
            <AppenderRef ref="info-log" />
        </Root>
        <Logger name="request" level="info"
                additivity="false">
            <AppenderRef ref="request-log"/>
        </Logger>
        <Logger name="org.springframework">
            <AppenderRef ref="Console" />
        </Logger>
    </Loggers>
</Configuration>

在application.yml中增加配置告知log4j2文件路徑

logging:
  config: classpath:log4j2-spring.xml

2 獲取POST的Body

記錄日志時通常關注請求URI、Method、QueryString、POST請求的Body、響應信息和來源IP等。對於Spring Cloud Gateway這其中的POST請求的Body獲取比較復雜,這里添加一個全局過濾器預先獲取並存入請求的Attributes中。
CachePostBodyFilter

@Component
public class CachePostBodyFilter implements GlobalFilter, Ordered {
 
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest serverHttpRequest = exchange.getRequest();
        String method = serverHttpRequest.getMethodValue();
        if("POST".equalsIgnoreCase(method)) {
            ServerRequest serverRequest = new DefaultServerRequest(exchange);
            Mono<String> bodyToMono = serverRequest.bodyToMono(String.class);
            return bodyToMono.flatMap(body -> {
                exchange.getAttributes().put("cachedRequestBody", body);
                ServerHttpRequest newRequest = new ServerHttpRequestDecorator(serverHttpRequest) {
                    @Override
                    public HttpHeaders getHeaders() {
                        HttpHeaders httpHeaders = new HttpHeaders();
                        httpHeaders.putAll(super.getHeaders());
                        httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
                        return httpHeaders;
                    }
 
                    @Override
                    public Flux<DataBuffer> getBody() {
                        NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(new UnpooledByteBufAllocator(false));
                        DataBuffer bodyDataBuffer = nettyDataBufferFactory.wrap(body.getBytes());
                        return Flux.just(bodyDataBuffer);
                    }
                };
                return chain.filter(exchange.mutate().request(newRequest).build());
            });
        }
        return chain.filter(exchange);
    }
 
    @Override
    public int getOrder() {
        return -21;
    }
}

3 記錄日志

接下來再創建一個過濾器用於記錄日志

@Component
public class LogFilter implements GlobalFilter, Ordered {
 
    static final Logger logger = LogManager.getLogger("request");
 
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
 
        StringBuilder logBuilder = new StringBuilder();
        ServerHttpRequest serverHttpRequest = exchange.getRequest();
        String method = serverHttpRequest.getMethodValue().toUpperCase();
        logBuilder.append(method).append(",").append(serverHttpRequest.getURI());
        if("POST".equals(method)) {
            String body = exchange.getAttributeOrDefault("cachedRequestBody", "");
            if(StringUtils.isNotBlank(body)) {
                logBuilder.append(",body=").append(body);
            }
        }
 
        ServerHttpResponse serverHttpResponse = exchange.getResponse();
        DataBufferFactory bufferFactory = serverHttpResponse.bufferFactory();
        ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(serverHttpResponse) {
            @Override
            public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                if (body instanceof Flux) {
                    Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
                    return super.writeWith(fluxBody.map(dataBuffer -> {
                        byte[] content = new byte[dataBuffer.readableByteCount()];
                        dataBuffer.read(content);
                        DataBufferUtils.release(dataBuffer);
                        String resp = new String(content, Charset.forName("UTF-8"));
                        logBuilder.append(",resp=").append(resp);
                        logger.info(logBuilder.toString());
                        byte[] uppedContent = new String(content, Charset.forName("UTF-8")).getBytes();
                        return bufferFactory.wrap(uppedContent);
                    }));
                }
                return super.writeWith(body);
            }
        };
        return chain.filter(exchange.mutate().response(decoratedResponse).build());
    }
 
    @Override
    public int getOrder() {
        return -20;
    }
}

五、鑒權

對請求的安全驗證方案視各自項目需求而定,沒有固定的做法,這里僅演示檢查簽名的處理。規則是:對除sign外所有請求參數按字典順序排序后組成key1=value1&key2=value2的字符串,然后計算MD5碼並與sign參數值比較,一致即認為通過。

這里面同樣要處理QueryString和POST方法的Body,因此和日志過濾器合並為在一起

@Component
public class AuthAndLogFilter implements GlobalFilter, Ordered {
 
    static final Logger logger = LogManager.getLogger("request");
 
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
 
        ServerHttpRequest serverHttpRequest = exchange.getRequest();
        ServerHttpResponse serverHttpResponse = exchange.getResponse();
 
        StringBuilder logBuilder = new StringBuilder();
        Map<String, String> params = parseRequest(exchange, logBuilder);
        boolean r = checkSignature(params, serverHttpRequest);
        if(!r) {
            Map map = new HashMap<>();
            map.put("code", 2);
            map.put("message", "簽名驗證失敗");
            String resp = JSON.toJSONString(map);
            logBuilder.append(",resp=").append(resp);
            logger.info(logBuilder.toString());
            DataBuffer bodyDataBuffer = serverHttpResponse.bufferFactory().wrap(resp.getBytes());
            serverHttpResponse.getHeaders().add("Content-Type", "text/plain;charset=UTF-8");
            return serverHttpResponse.writeWith(Mono.just(bodyDataBuffer));
        }
 
        DataBufferFactory bufferFactory = serverHttpResponse.bufferFactory();
        ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(serverHttpResponse) {
            @Override
            public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                if (body instanceof Flux) {
                    Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
                    return super.writeWith(fluxBody.map(dataBuffer -> {
                        byte[] content = new byte[dataBuffer.readableByteCount()];
                        dataBuffer.read(content);
                        DataBufferUtils.release(dataBuffer);
                        String resp = new String(content, Charset.forName("UTF-8"));
                        logBuilder.append(",resp=").append(resp);
                        logger.info(logBuilder.toString());
                        byte[] uppedContent = new String(content, Charset.forName("UTF-8")).getBytes();
                        return bufferFactory.wrap(uppedContent);
                    }));
                }
                return super.writeWith(body);
            }
        };
        return chain.filter(exchange.mutate().response(decoratedResponse).build());
    }
 
    private Map<String, String> parseRequest(ServerWebExchange exchange, StringBuilder logBuilder) {
        ServerHttpRequest serverHttpRequest = exchange.getRequest();
        String method = serverHttpRequest.getMethodValue().toUpperCase();
        logBuilder.append(method).append(",").append(serverHttpRequest.getURI());
        MultiValueMap<String, String> query = serverHttpRequest.getQueryParams();
        Map<String, String> params = new HashMap<>();
        query.forEach((k, v) -> {
            params.put(k, v.get(0));
        });
        if("POST".equals(method)) {
            String body = exchange.getAttributeOrDefault("cachedRequestBody", "");
            if(StringUtils.isNotBlank(body)) {
                logBuilder.append(",body=").append(body);
                String[] kvArray = body.split("&");
                for (String kv : kvArray) {
                    if (kv.indexOf("=") >= 0) {
                        String k = kv.split("=")[0];
                        String v = kv.split("=")[1];
                        if(!params.containsKey(k)) {
                            try {
                                params.put(k, URLDecoder.decode(v, "UTF-8"));
                            } catch (UnsupportedEncodingException e) {
                            }
                        }
                    }
                }
            }
        }
        return params;
    }
 
    private boolean checkSignature(Map<String, String> params, ServerHttpRequest serverHttpRequest) {
 
        String sign = params.get("sign");
        if(StringUtils.isBlank(sign)) {
            return false;
        }
        //檢查簽名
        Map<String, String> sorted = new TreeMap<>();
        params.forEach( (k, v) -> {
            if(!"sign".equals(k)) {
                sorted.put(k, v);
            }
        });
        StringBuilder builder = new StringBuilder();
        sorted.forEach((k, v) -> {
            builder.append(k).append("=").append(v).append("&");
        });
        String value = builder.toString();
        value = value.substring(0, value.length() - 1);
        if(!sign.equalsIgnoreCase(MD5Utils.MD5(value))) {
            return false;
        }
 
        return true;
    }
 
    @Override
    public int getOrder() {
        return -20;
    }
}

測試

A:無簽名

B:帶簽名GET請求

 

 C:POST請求

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM