spring cloud alibaba实用攻略一 Gateway响应式实战案例

Gateway实战案例

  • NACOS配置
    • 集成基于security oauth2 开发的4A系统
    • 配置服务路由
    • 应用支撑
  • 程序配置类
    • 跨域
    • 错误处理配置
      • 记录信息类
    • 网关日志记录配置
    • ip限流配置
      • redis配置
  • 关于集成权限部分内容较多下次再写

Gateway最佳实践
配置

server:
  port: 5020

spring:
  application:
    name: test-gateway

  cloud:
    nacos:
      discovery:
        namespace: 645cae0c-cf99-459d-87e1-9a288de8a75j
        server-addr: 192.168.0.183:8848
        cluster-name: test-platform
        service: ${
     spring.application.name}
        ip: ${
     spring.cloud.client.ip-address}
        port: ${
     server.port}

      config:
        namespace: 645cae0c-cf99-459d-87e1-9a288de8a75j
        server-addr: 192.168.0.183:8848
        cluster-name: test-platform
        ext-config[0]:
          group: gateway-manage
          data-id: feign-gateway-${
     spring.profiles.active}.yml
        ext-config[1]:
          group: gateway-manage
          data-id: redis-gateway-${
     spring.profiles.active}.yml
        ext-config[2]:
          group: gateway-manage
          data-id: routes-${
     spring.profiles.active}.yml
        ext-config[3]:
          group: gateway-manage
          data-id: security-${
     spring.profiles.active}.yml
          refresh: true
        ext-config[4]:
          group: gateway-manage
          data-id: permission-${
     spring.profiles.active}.yml
          refresh: true

config:
  basic:
    cors-enabled: false #设置网关的跨域因为是reactive环境 所以basic包中的cors配置无效 需要禁用并单独配置 见CorsConfig.java
    time-serializer-enabled: false

security:
  h4a.client:
    configPath: /home/gateway/config/ #集成海关H4A配置

NACOS配置

集成基于security oauth2 开发的4A系统

security-dev.yml

security:
  oauth2:
    url:
      serverUrl: http://test.taylor.net/4a
      clientUrl: http://test.taylor.net/api/
    client:
      #server
      userAuthorizationUrl: ${
     security.oauth2.url.serverUrl}/oauth/authorize
      accessTokenUrl: ${
     security.oauth2.url.serverUrl}/oauth/token
      ssoLogoutUrl: ${
     security.oauth2.url.serverUrl}/logout
      refreshTokenUrl: ${
     security.oauth2.url.serverUrl}/server/refreshToken
      menuTreeUrl: ${
     security.oauth2.url.serverUrl}/server/getMenuTree
      permissionsUrl: ${
     security.oauth2.url.serverUrl}/server/getPermissions
      #client
      clientId:
        - TEST-V3
      clientSecret:
        - 59b8549f75f9476fbd1e4dccf71c302c
      clientUrl:
        - http://test.taylor.net
      logoutUrl: ${
     security.oauth2.url.clientUrl}/logout
      redirectUrl:
      tokenStoreTime: 1800
      signKey: 954c7ca837a446328abe84852245e270
      allowUrl: /login,/logout,/ssoLogout

配置服务路由

routes-dev.yml

spring:
  cloud:
    gateway:
      default-filters:
        - name: StripPrefix
          args:
            parts: 1
        - name: Hystrix
          args:
            name: default-gateway-fallback
            fallbackUri: forward:/fallback
      discovery:
        locator:
          enabled: false
          lower-case-service-id: true
      routes:
      #service1
      - id: platform-service1
        uri: lb://taylor-platform-service1
        predicates:
        - Path=/service1/**
     #service2
      - id: platform-service2
        uri: lb://taylor-platform-service2
        predicates:
        - Path=/service2/**
     

应用支撑

redis

spring:
  redis:
    database: 0
    password: dev
    port: 6379
    timeout: 3000ms
    jedis:
      pool:
        max-idle: 8
        min-idle: 1
        max-active: 8
        max-wait: 5000ms
    sentinel:
      master: mymaster
      nodes: 192.168.0.15:26379,192.168.0.15:26479,192.168.0.15:26579

httpclient

http:
 config:
  connectTimeout: 2000
  readTimeout: 2000
  writeTimeout: 2000
  connectionPool:
    maxIdleConnections: 6
    keepAliveDuration: 6

feign-gateway

spring.cloud.loadbalancer.retry.enabled: false
#feign.sentinel.enabled: true
feign.hystrix.enabled: true
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 8000
ribbon:ConnectTimeout: 7500
ribbon.ReadTimeout: 7500
ribbon.OkToRetryOnAllOperations: false
ribbon.MaxAutoRetriesNextServer: 0
ribbon.MaxAutoRetries: 0
management.endpoints.web.exposure.include: "*"

程序配置类

跨域

package com.taylor.gateway.config.cors;

import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsUtils;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;

import java.util.Objects;

/**
 *
 * 设置网关的跨域
 * 因为是reactive环境 所以basic包中的cors配置无效 需要禁用并单独配置
 */
@Configuration
public class CorsConfig implements WebFilter, Ordered {
     

    private static final String MAX_AGE = "1800";

    @Override
    public int getOrder() {
     
        return -30;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
     
        ServerHttpRequest request = exchange.getRequest();
        if (!CorsUtils.isCorsRequest(request)) {
     
            return chain.filter(exchange);
        }
        HttpHeaders requestHeaders = request.getHeaders();
        ServerHttpResponse response = exchange.getResponse();
        HttpMethod requestMethod = requestHeaders.getAccessControlRequestMethod();
        HttpHeaders headers = response.getHeaders();
        headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, requestHeaders.getOrigin());
        headers.addAll(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, requestHeaders.getAccessControlRequestHeaders());
        if (Objects.nonNull(requestMethod)) {
     
            headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, requestMethod.name());
        }
        headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
        headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, CorsConfiguration.ALL);
        headers.add(HttpHeaders.ACCESS_CONTROL_MAX_AGE, MAX_AGE);
        if (request.getMethod() == HttpMethod.OPTIONS) {
     
            response.setStatusCode(HttpStatus.OK);
            return Mono.empty();
        }
        return chain.filter(exchange);
    }
}

错误处理配置

FallbackHandler

package com.taylor.gateway.config.exception;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 *
 * 处理请求路由的熔断降级
 *
 *
 */
@RestController
public class FallbackHandler {
     

//    @RequestMapping("/fallback")
//    public Mono fallback() {
     
//        log.warn("===> Gateway timeout [{} {} {}]");
//        return Mono.just(Result.build(Status.FAILURE, "网关处理超时"));
//    }

    /**
     * 请求超时会执行降级 但是没有可用的服务实例时也会进入降级
     * 为了在日志中更精确的区分是超时还是服务不可用 在降级方法中抛出异常 交由ErrorHandler去处理
     */
    @RequestMapping("/fallback")
    public void fallback() {
     
        throw new FallbackException("当前请求无法完成处理,请稍后再试");
    }
}

ErrorWebExceptionHandler 配置

package com.taylor.gateway.config.error;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.web.ResourceProperties;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.context.ApplicationContext;
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.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.result.view.ViewResolver;

import java.util.Collections;
import java.util.List;

/**
 *
 * 覆盖默认的错误处理类
 */
@Configuration
@EnableConfigurationProperties({
     ServerProperties.class, ResourceProperties.class})
public class ErrorHandlerConfig {
     

    private final ServerProperties serverProperties;

    private final ApplicationContext applicationContext;

    private final ResourceProperties resourceProperties;

    private final List<ViewResolver> viewResolvers;

    private final ServerCodecConfigurer serverCodecConfigurer;

    public ErrorHandlerConfig(ServerProperties serverProperties,
                                  ResourceProperties resourceProperties,
                                  ObjectProvider<List<ViewResolver>> viewResolversProvider,
                                  ServerCodecConfigurer serverCodecConfigurer,
                                  ApplicationContext applicationContext) {
     
        this.serverProperties = serverProperties;
        this.applicationContext = applicationContext;
        this.resourceProperties = resourceProperties;
        this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
        this.serverCodecConfigurer = serverCodecConfigurer;
    }

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes) {
     
        ErrorWebExceptionHandler exceptionHandler = new ErrorWebExceptionHandler(
                errorAttributes,
                this.resourceProperties,
                this.serverProperties.getError(),
                this.applicationContext);
        exceptionHandler.setViewResolvers(this.viewResolvers);
        exceptionHandler.setMessageWriters(this.serverCodecConfigurer.getWriters());
        exceptionHandler.setMessageReaders(this.serverCodecConfigurer.getReaders());
        return exceptionHandler;
    }
}


package com.taylor.gateway.config.error;

import com.netflix.hystrix.exception.HystrixRuntimeException;
import com.netflix.hystrix.exception.HystrixTimeoutException;
import com.taylor.gateway.constant.ServerRequestConstant;
import com.taylor.gateway.record.AccessRecord;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.web.ErrorProperties;
import org.springframework.boot.autoconfigure.web.ResourceProperties;
import org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWebExceptionHandler;
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.cloud.gateway.support.TimeoutException;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpStatus;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ResponseStatusException;

import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

/**
 *
 * 处理请求路由过程中的异常
 *
 */
@Slf4j
public class ErrorWebExceptionHandler extends DefaultErrorWebExceptionHandler {
     

    public ErrorWebExceptionHandler(ErrorAttributes errorAttributes, ResourceProperties resourceProperties, ErrorProperties errorProperties, ApplicationContext applicationContext) {
     
        super(errorAttributes, resourceProperties, errorProperties, applicationContext);
    }

    @Override
    protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
     
        return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
    }


    @Override
    protected Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) {
     
        //获取错误信息
        Throwable throwable = super.getError(request);
        //计算请求开始时间
        Optional<Object> optional = request.attribute(ServerRequestConstant.REQUEST_TIME_BEGIN);
        LocalDateTime startTime = optional.isPresent() ? LocalDateTime
                .ofEpochSecond((Long) optional.get() / 1000, 0, ZoneOffset.ofHours(8))
                : LocalDateTime.now();
        //请求信息记录
        AccessRecord record = new AccessRecord(request);
        record.setStartTime(startTime);
        record.setHttpStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        record.setResult(throwable.getMessage() == null ? throwable.toString() : throwable.getMessage());
        record.setEndTime(LocalDateTime.now());
        //此处的状态只表示gateway
        //比如 此处的404是gateway找不到路由mapping时的状态 而不是具体服务实例返回的状态
        if(throwable instanceof ResponseStatusException) {
     
            HttpStatus status = ((ResponseStatusException) throwable).getStatus();
            record.setHttpStatus(status.value());
            if(status == HttpStatus.NOT_FOUND) {
     
                return buildResult(status.value(), "未知的请求路径");
            } else if (status == HttpStatus.BAD_REQUEST) {
     
                return buildResult(status.value(), Objects.isNull(throwable.getMessage())
                        ? "请求无法进行处理" : throwable.getMessage());
            }
            return buildResult(status.value(), "请求处理失败");
        }
        //是否是超时类异常
        boolean rs = (throwable instanceof HystrixRuntimeException
                        && throwable.getCause() instanceof HystrixTimeoutException)
                        || throwable instanceof TimeoutException;
        if(rs) {
     
            record.setResult("Read timed out");
            return buildResult(HttpStatus.INTERNAL_SERVER_ERROR.value(), "请求处理超时");
        }
        //其它类型的异常统一处理
        return buildResult(HttpStatus.INTERNAL_SERVER_ERROR.value(),
                throwable.getMessage() == null ? "网关处理发生错误" : throwable.getMessage());
    }

    @Override
    protected void logError(ServerRequest request, HttpStatus errorStatus) {
     
        Throwable throwable = super.getError(request);
        //如果是找不到路由 则打印日志 不打印栈信息
        if(errorStatus == HttpStatus.NOT_FOUND) {
     
            log.warn(buildMessage(request, throwable), throwable);
            return;
        }
        //熔断类和超时类异常也不打印栈信息
        if(throwable instanceof HystrixRuntimeException || throwable instanceof TimeoutException) {
     
            Throwable cause = throwable.getCause();
            log.error(buildMessage(request, cause == null ? new TimeoutException("Read timed out") : cause), throwable);
            return;
        }
        //其它类型异常统一打印
        log.error(buildMessage(request, throwable), throwable);
    }

    private Map<String, Object> buildResult(int status, String msg) {
     
        Map<String, Object> params = new HashMap<>(4);
        params.put("status", status);
        params.put("msg", msg);
        params.put("page", null);
        params.put("data", null);
        return params;
    }

    private String buildMessage(ServerRequest request, Throwable ex) {
     
        //此处可以增加打印其它的信息
        //因为webflux本身API的问题 暂时没有办法获取到IP地址
        StringBuilder message = new StringBuilder("Failed to handle request [");
        message.append(request.methodName());
        message.append(" ");
        message.append(request.uri());
        message.append("]");
        if(ex != null) {
     
            message.append(": ");
            message.append(ex.getMessage() == null ? ex.toString() : ex.getMessage());
        }
        return message.toString();
    }
}

记录信息类

package com.taylor.gateway.record;

import com.alibaba.fastjson.JSON;
import com.taylor.gateway.constant.ServerRequestConstant;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.reactive.function.server.ServerRequest;

import java.time.LocalDateTime;

/**
 *
 * 用于记录网关的请求
 *
 */
@Getter
@Setter
@ToString
@NoArgsConstructor
public class AccessRecord {
     

    private String ipAddress;

    private String url;

    private String method;

    private String headers;

    private String bodyParams;

    private String queryParams;

    private Integer httpStatus;

    private String result;

    private LocalDateTime startTime;

    private LocalDateTime endTime;

    private Long millisecond;

    private String exception;


    public AccessRecord(ServerRequest request) {
     
        this.ipAddress = (String) request.attribute(ServerRequestConstant.ADDRESS_HEADER).orElse("IP Address is Null");
        this.url = request.uri().toString();
        this.method = request.methodName();
        this.headers = JSON.toJSONString(request.headers().asHttpHeaders());
        this.queryParams = JSON.toJSONString(request.queryParams());
    }

    public AccessRecord(ServerHttpRequest request) {
     
        this.url = request.getURI().toString();
        this.method = request.getMethodValue();
        this.headers = JSON.toJSONString(request.getHeaders());
        this.queryParams = JSON.toJSONString(request.getQueryParams());
    }
}
package com.taylor.gateway.constant;

/**
 *
 * HTTP相关常量
 *
 */
public class ServerRequestConstant {
     

    /**
     * 请求开始时间KEY
     */
    public static final String REQUEST_TIME_BEGIN = "Request-Start-Time";

    /**
     * 请求客户端的IP地址HEAD
     */
    public static final String ADDRESS_HEADER = "Client-IP-Address";

    /**
     * 请求协议
     */
    public static final String HTTP_HEAD = "HTTP";

    /**
     * 请求协议
     */
    public static final String HTTPS_HEAD = "HTTPS";

    /**
     * 请求方式
     */
    public static final String REQUEST_METHOD_POST = "POST";
}

网关日志记录配置

webflux暂不支持mysql 因此只记录在日志中
直到 Spring Boot 2.3.0.RELEASE,才正式支持基于 r2dbc 的 MySQL 驱动。后续有空再更新 https://www.jianshu.com/p/28bec31ae72c

package com.taylor.gateway.filter;


import com.taylor.gateway.constant.ServerRequestConstant;
import com.taylor.gateway.record.AccessRecord;
import com.taylor.gateway.record.Records;
import com.taylor.gateway.util.ServerUtil;
import com.taylor.gateway.util.StringUtil;
import io.netty.buffer.UnpooledByteBufAllocator;
import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.support.DefaultServerRequest;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Objects;
import java.util.UUID;

/**
 *
 * 记录网关的请求日志信息
 */
@Slf4j
@Component
public class LoggerGlobalFilter implements GlobalFilter, Ordered {
     

    @Override
    public int getOrder() {
     
        return -5;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
     
        //验证类型
        String scheme = exchange.getRequest().getURI().getScheme().toUpperCase();
        boolean rs = ServerRequestConstant.HTTP_HEAD.equals(scheme) || ServerRequestConstant.HTTPS_HEAD.equals(scheme);
        if(!rs) {
     
            return chain.filter(exchange);
        }
        ServerHttpRequest request = exchange.getRequest();
        String ipAddress = ServerUtil.ipAddress(request);
        //存放请求相关信息
        exchange.getAttributes().put(ServerRequestConstant.REQUEST_TIME_BEGIN, System.currentTimeMillis());
        exchange.getAttributes().put(ServerRequestConstant.ADDRESS_HEADER, ipAddress);
        //记录请求的基本信息
        AccessRecord record = new AccessRecord(request);
        record.setStartTime(LocalDateTime.now());
        record.setIpAddress(ipAddress);
        //记录请求体的类型 上传文件类型的请求单独处理
        //GET请求不存在请求体 只记录POST类型
        // TODO: 2019/12/18 需要支持MULTIPART_FORM_DATA的处理
        String contentType = request.getHeaders().getFirst("Content-Type");
        boolean res =
                Objects.nonNull(contentType)
                && ServerRequestConstant.REQUEST_METHOD_POST.equals(request.getMethodValue())
                && !contentType.startsWith(MediaType.MULTIPART_FORM_DATA_VALUE);
        if(res) {
     
            //POST方式 读取请求体
            //requestBody具有只能被读取一次的特性 所以需要在记录之后重新包装一个新的request对象
            //因为gateway是基于reactive开发的原因
            //导致遇到很多问题 目前的实现方式能够避免一下问题 暂不知后续是否还有问题
            //1.请求体只能读取一次
            //2.请求体超过1024B被截断
            //3.请求体为空时无法获取到响应
            ServerHttpRequest mutatedRequest = resolveRequest(exchange, record);
            ServerHttpResponse mutatedResponse = resolveResponse(exchange, record);
            return filter(exchange.mutate().request(mutatedRequest).response(mutatedResponse).build(), chain, record);
        }
        //GET方式直接使用原有对象
        ServerHttpResponse mutatedResponse = resolveResponse(exchange, record);
        return filter(exchange.mutate().response(mutatedResponse).build(), chain, record);
    }

    /**
     * 记录响应之后的耗时信息
     * 处理访问请求记录
     * @param exchange
     * @param chain
     * @return
     */
    private Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain, AccessRecord record) {
     
        return chain.filter(exchange).then(
                Mono.fromRunnable(() -> {
     
                    Long startTime = exchange.getAttribute(ServerRequestConstant.REQUEST_TIME_BEGIN);
                    //计算耗时
                    //如果exchange中没有获取到REQUEST_TIME_BEGIN 则使用记录中的开始时间
                    if(Objects.isNull(startTime)) {
     
                        //转换为毫秒数
                        startTime = record.getStartTime().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
                        //为了防止输出的日志太多 此处只打印部分日志
                        //同时添加一个ID 后续可以通过此ID获取完整的记录
                        String recordId = UUID.randomUUID().toString().replaceAll("-", "");
                        record.setException(recordId);
                        log.error("===> {} {} {} request attribute[requestTimeBegin] not found",
                                record.getIpAddress(), record.getUrl(), recordId);
                    }
                    record.setEndTime(LocalDateTime.now());
                    record.setMillisecond(System.currentTimeMillis() - startTime);
                    if (exchange.getResponse().getStatusCode() == HttpStatus.TOO_MANY_REQUESTS) {
     
                        //限流请求
                        record.setHttpStatus(HttpStatus.TOO_MANY_REQUESTS.value());
                        record.setResult("操作太频繁, 触发请求限流");
                    }
                    //处理请求记录
                    Records.handler(record);
                })
        );
    }

    /**
     * 处理请求体
     * 处理之后封装为新的request
     * @param exchange
     * @param record
     * @return
     */
    private ServerHttpRequest resolveRequest(ServerWebExchange exchange, AccessRecord record) {
     
        DefaultServerRequest serverRequest = new DefaultServerRequest(exchange);
        ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
     
            //记录请求体内容
            private StringBuilder builder = new StringBuilder();
            //因为没有对请求体进行操作 所以这里重写只是为了加上TRANSFER_ENCODING的请求头
            //TRANSFER_ENCODING是用来表示分块传输 目前不知道具体意义
            //因为读取方式是参考的源码类ModifyRequestBodyGatewayFilterFactory
            //此类也进行了重写 为了防止未知问题
            @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() {
     
                return serverRequest.bodyToFlux(String.class).map(body -> {
     
                    builder.append(body);
                    NettyDataBufferFactory nettyDataBufferFactory =
                        new NettyDataBufferFactory(new UnpooledByteBufAllocator(false));
                    return nettyDataBufferFactory.wrap(body.getBytes(StandardCharsets.UTF_8));
                }).doOnComplete(()-> record.setBodyParams(StringUtil.format(builder.toString())));
            }
        };
        return mutatedRequest;
    }

    /**
     * 处理并读取response
     * 读取之后需要封装为新的response
     * @param exchange
     * @param record
     * @return
     */
    private ServerHttpResponse resolveResponse(ServerWebExchange exchange, AccessRecord record) {
     
        ServerHttpResponse response = exchange.getResponse();
        DataBufferFactory bufferFactory = response.bufferFactory();
        ServerHttpResponseDecorator mutatedResponse = new ServerHttpResponseDecorator(response) {
     
            //记录响应体内容
            //应该也会出现响应体过长导致的截断问题
            //测试结果 当响应体过长时 会多次读取 前端收到的结果是完整的 记录日志时 需要多次append才能得到完整的数据

            //记录响应体内容
            private StringBuilder builder = new StringBuilder();

            @Override
            public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
     
                //记录请求执行状态
                //只能写在读取方法中 因为此时才收到了响应
                record.setHttpStatus(response.getStatusCode() != null ? response.getStatusCode().value() : null);
                /*************************************************/
                if (body instanceof Flux) {
     
                    Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
                    return super.writeWith(fluxBody.map(dataBuffer -> {
     
                        byte[] bytes = new byte[dataBuffer.readableByteCount()];
                        dataBuffer.read(bytes);
                        DataBufferUtils.release(dataBuffer);
                        String resp = new String(bytes, StandardCharsets.UTF_8);
                        builder.append(resp);
                        record.setResult(StringUtil.format(builder.toString()));
                        return bufferFactory.wrap(bytes);
                    }));
                }
                /*************************************************/
                return super.writeWith(body);
            }
        };
        return mutatedResponse;
    }
}

三个辅助类

package com.taylor.gateway.record;

import lombok.extern.slf4j.Slf4j;

/**
 *
 * 请求访问记录处理 todo Spring Boot 2.3.0.RELEASE,正式支持基于 r2dbc 的 MySQL 驱动
 */
@Slf4j
public class Records {
     

    public static void handler(AccessRecord record) {
     
      log.info(record.toString());
    }
}

package com.taylor.gateway.util;

import org.springframework.util.StringUtils;

import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 *
 * 字符串相关操作
 */
public class StringUtil {
     

    private static final Pattern PATTERN = Pattern.compile("\\s*|\t|\r|\n");

    /**
     * 去掉空格,换行和制表符
     * @param str
     * @return
     */
    public static String format(String str){
     
        if (!StringUtils.isEmpty(str)) {
     
            Matcher m = PATTERN.matcher(str);
            return m.replaceAll("");
        }
        return str;
    }

    /**
     * 去除权限信息中重复的数据
     * @param auth
     * @return
     */
    public static String deduplication(String auth) {
     
        String[] authArr = auth.split(",");
        return Stream.of(authArr).distinct().collect(Collectors.joining(","));
    }
}


package com.taylor.gateway.util;

import io.netty.buffer.ByteBufAllocator;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;

/**
 *
 * 获取ServerHttpRequest中的信息
 *
 */
public class ServerUtil {
     
    private static final String HEADER_VALUE_UNKNOWN = "UNKNOWN";

    private ServerUtil(){
     }

    /**
     * webflux的request用此方式获取
     * 从header中获取 通过代理访问后使用默认api获取的地址不准确
     * @param request
     * @return
     */
    public static String ipAddress(ServerHttpRequest request) {
     
//        InetSocketAddress remoteAddress = request.getRemoteAddress();
//        if(Objects.isNull(remoteAddress)) {
     
//            return "remoteAddress is Null";
//        }
//        String hostAddress = remoteAddress.getAddress().getHostAddress();
//        return hostAddress;
        HttpHeaders headers = request.getHeaders();
        String ipVal = headers.getFirst("x-forwarded-for");
        if (StringUtils.isNotEmpty(ipVal) && ! HEADER_VALUE_UNKNOWN.equalsIgnoreCase(ipVal)) {
     
            boolean isContains = ipVal.contains(",");
            if (isContains) {
     
                ipVal = ipVal.split(",")[0];
            }
            return ipVal;
        }
        if (StringUtils.isEmpty(ipVal) || HEADER_VALUE_UNKNOWN.equalsIgnoreCase(ipVal)) {
     
            ipVal = headers.getFirst("Proxy-Client-IP");
        }
        if (StringUtils.isEmpty(ipVal) || HEADER_VALUE_UNKNOWN.equalsIgnoreCase(ipVal)) {
     
            ipVal = headers.getFirst("WL-Proxy-Client-IP");
        }
        if (StringUtils.isEmpty(ipVal) || HEADER_VALUE_UNKNOWN.equalsIgnoreCase(ipVal)) {
     
            ipVal = headers.getFirst("HTTP_CLIENT_IP");
        }
        if (StringUtils.isEmpty(ipVal) || HEADER_VALUE_UNKNOWN.equalsIgnoreCase(ipVal)) {
     
            ipVal = headers.getFirst("HTTP_X_FORWARDED_FOR");
        }
        if (StringUtils.isEmpty(ipVal) || HEADER_VALUE_UNKNOWN.equalsIgnoreCase(ipVal)) {
     
            ipVal = headers.getFirst("X-Real-IP");
        }
        if (StringUtils.isEmpty(ipVal) || HEADER_VALUE_UNKNOWN.equalsIgnoreCase(ipVal)) {
     
            ipVal = request.getRemoteAddress().getAddress().getHostAddress();
        }
        return ipVal;
    }

    /**
     * 读取request中的请求体
     * @param request
     * @return
     */
    public static String resolve(ServerHttpRequest request) {
     
        StringBuilder sb = new StringBuilder();
        request.getBody().subscribe(buffer -> {
     
            byte[] bytes = new byte[buffer.readableByteCount()];
            buffer.read(bytes);
            DataBufferUtils.release(buffer);
            String body = new String(bytes, StandardCharsets.UTF_8);
            sb.append(body);
        });
        return sb.toString();
    }

    /**
     * 还原body为DataBuffer
     * @param body
     * @return
     */
    public static DataBuffer restore(String body) {
     
        byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
        NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
        DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length);
        buffer.write(bytes);
        return buffer;
    }

    /**
     * 使用response写出信息
     * @param response
     * @param msg
     * @return
     */
    public static Mono<Void> write(ServerHttpResponse response, String msg) {
     
        return write(response, HttpStatus.OK, msg);
    }

    /**
     * 使用response写出信息
     * @param response
     * @param msg
     * @return
     */
    public static Mono<Void> write(ServerHttpResponse response, HttpStatus status, String msg) {
     
        int value = status.value();
        response.setStatusCode(status);
        return response.writeWith(Flux.just(ServerUtil.restore(ResultUtil.buildResultJSON(value, msg))));
    }
}

ip限流配置

package com.taylor.gateway.config.ratelimit;

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

/**
 *
 * 根据请求IP进行限流
 *
 */
@Configuration
public class HostKeyResolver implements KeyResolver {
     

    @Override
    public Mono<String> resolve(ServerWebExchange exchange) {
     
        return Mono.just(ServerUtil.ipAddress(exchange.getRequest()));
    }
}

redis配置

package com.taylor.gateway.config.redis;

import com.alibaba.fastjson.support.spring.FastJsonRedisSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * Created by xxon 2017/11/3.
 *
 *   redis配置
 */
@Configuration
public class RedisConfig {
     

    @Bean("redisTemplate")
    @SuppressWarnings("unchecked")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
     
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate();
        redisTemplate.setConnectionFactory(factory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new FastJsonRedisSerializer(Object.class));
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

关于集成权限部分内容较多下次再写

你可能感兴趣的:(知识总结,cloud,gateway,oauth,网关,java)