• 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请求

  • 相关阅读:
    LeetCode 8 有效的括号
    String源码学习
    LeetCode 7最长公共前缀
    LeetCode 5回文数
    LeetCode 6罗马数字转整数
    LeetCode 4.反转整数
    LeetCode 3.无重复字符的最长子串
    区分子串和子序列
    LeetCode 1.两数之和
    一个十分好用的动画工具:Velocity.js
  • 原文地址:https://www.cnblogs.com/h-z-y/p/14577541.html
Copyright © 2020-2023  润新知