美文网首页
springcloud 之 gateway

springcloud 之 gateway

作者: 安静的夜灬 | 来源:发表于2020-08-26 09:31 被阅读0次

    值得一提的是低版本的 springcloud 网关是 zuulspringboot 2.x 新版本的网关是 gateway

    通过本文你可以学到2点:

    • 如何在网关层重写 http 请求与响应
    • 如何通过代码的方式配置路由

    一、网关的引入用来解决什么问题

    gateway 主要做2件事:

    • 路由
    • 过滤

    二、如何使用路由

    配置文件的方式使用路由的例子很多,这里我不再赘述,这里我只打算写一下通过代码完成路由配置

    AuthAutoConfiguration 创建一个配置文件

    import com.flashwhale.cloud.gateway.filter.EncryptionFilter;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.cloud.gateway.route.RouteLocator;
    import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import javax.ws.rs.HttpMethod;
    import java.util.Objects;
    
    /**
     * 这里进行转发路由的配置
     * 注意,如果路由需要有顺序匹配的,需要使用 order 方法 值越小优先匹配
     */
    @Slf4j
    @Configuration
    public class AuthAutoConfiguration {
     
        @Bean
        public RouteLocator routeLocator(RouteLocatorBuilder builder) {
            return builder.routes()
                    /**
                     * 针对 cloud-gateway 提供 dh 服务的普通路由
                     */
                    .route("cloud-gateway-dh", r ->
                            r
                                    .path("/dh/**")
                                    .uri("lb://cloud-gateway")
                    )
                    /**
                     * 针对 user-server 服务的普通路由
                     */
                    .route("user-server", r ->
                            r
                                    .path("/user_api/**")
                                    .uri("lb://user-server")
                    )
                    /**
                     * 针对 user-web 服务的普通路由
                     */
                    .route("user-web", r ->
                            r
                                    .path("/user_web/**")
                                    .uri("lb://user-web")
                    )
                    .build();
        }
    }
    
    说明:
    • 一个 .route 就是一个路由规则
    • 路由中的 id 是唯一的,不一定需要与服务名对应,不重复即可
    • path 方法匹配是根据路径来的,实际上是一个通配,所以这里需要在对应的服务前加上前缀,类似这样:
    server:
      servlet:
        context-path: /user_api
    
    • uri 方法是必须的,必须声明一个地址,通过 lb://服务名 可以使用到 eureka 负载均衡的转发特性,当然也可以使用类似 http://localhost:8080 等方式

    二、如何使用过滤

    同样的配置文件的方式使用过滤的例子很多,这里我不再赘述,这里我只打算写一下通过代码完成过滤配置
    • EncryptionFilter 编写一个网关过滤器,这里不是全局的,当前演示的过滤器是修改请求和响应体完成加密解密的过程,有关 DH 的部分请参考dh秘钥交换算法实践 如果这篇文章不可见,请移步我的公众号查看,简书的规则不知道是什么,就锁定了这篇文章。
    import com.fasterxml.jackson.core.JsonProcessingException;
    import com.fasterxml.jackson.databind.JsonNode;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.flashwhale.cloud.gateway.common.DHProperties;
    import com.flashwhale.cloud.gateway.common.DTO.DataDTO;
    import com.flashwhale.cloud.gateway.utlis.AESUtil;
    import com.flashwhale.cloud.gateway.utlis.dh.server.DHService;
    import io.netty.buffer.ByteBufAllocator;
    import lombok.SneakyThrows;
    import lombok.extern.slf4j.Slf4j;
    import org.reactivestreams.Publisher;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.cloud.gateway.filter.GatewayFilter;
    import org.springframework.cloud.gateway.filter.GatewayFilterChain;
    import org.springframework.cloud.gateway.filter.NettyWriteResponseFilter;
    import org.springframework.core.Ordered;
    import org.springframework.core.annotation.Order;
    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.util.StringUtils;
    import org.springframework.web.server.ServerWebExchange;
    import org.springframework.web.util.UriComponentsBuilder;
    import reactor.core.publisher.Flux;
    import reactor.core.publisher.Mono;
    
    import javax.ws.rs.HttpMethod;
    import java.io.InputStream;
    import java.net.URI;
    import java.nio.CharBuffer;
    import java.nio.charset.StandardCharsets;
    import java.util.concurrent.atomic.AtomicReference;
    
    /**
     * 加密/解密输出内容
     */
    @Component
    @Slf4j
    public class EncryptionFilter implements GatewayFilter, Ordered {
    
        @Autowired
        ObjectMapper objectMapper;
        @Autowired
        DHService dhService;
        @Autowired
        DHProperties dhProperties;
    
    
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            ServerHttpRequest request = exchange.getRequest();
            ServerHttpResponse response = exchange.getResponse();
            String urlPath = request.getURI().getPath();
            String schema = request.getURI().getScheme();
            if ((!"http".equals(schema) && !"https".equals(schema))) {
                log.warn("当前访问【:{}】不是http请求", urlPath);
                return chain.filter(exchange);
            }
            if (!request.getMethod().matches(HttpMethod.POST)) {
                log.warn("当前访问【:{}】不是http post 请求", urlPath);
                return chain.filter(exchange);
            }
            if (!dhProperties.getOpen()) {
                log.debug("当前访问 【:{}】没有启用全局加密/解密", request.getURI());
                return chain.filter(exchange);
            }
            log.info("当前访问【:{}】", urlPath);
            //todo  暂时不使用这种方式获取 body数据
    //      LinkedHashMap<String, Object> requestBodyMap = exchange.getAttribute("cachedRequestBodyObject");
            String bodyStr = resolveBodyFromRequest(request);
            JsonNode parametersJson = null;
            try {
                parametersJson = objectMapper.readTree(bodyStr);
            } catch (JsonProcessingException e) {
                log.error("当前访问 【:{}】解析 body 数据发生异常", urlPath, e);
                response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
                return chain.filter(exchange);
            }
            String uuid = parametersJson.get(DataDTO.UUID_NAME).asText();
            String parameters = parametersJson.get(DataDTO.PARAMETER_NAME).asText();
            log.info("获取的值 requestBody:{}", bodyStr);
            ServerHttpRequestDecorator requestDecorator = processRequest(uuid, parameters, request, response);
           ServerHttpResponseDecorator responseDecorator = processResponse(response, uuid, urlPath);
     return chain.filter(exchange.mutate().request(requestDecorator).response(responseDecorator).build());
       //     return chain.filter(exchange.mutate().request(requestDecorator).build());
        }
    
        /**
         * 构造 request
         *
         * @param uuid       uuid
         * @param parameters 要解密的信息
         * @param request    ServerHttpRequest
         * @param response   ServerHttpResponse
         * @return 构造的 CustomRequestDecorator
         */
        ServerHttpRequestDecorator processRequest(String uuid,
                                                  String parameters,
                                                  ServerHttpRequest request,
                                                  ServerHttpResponse response) {
            URI uri = request.getURI();
            URI ex = UriComponentsBuilder.fromUri(uri).build(true).toUri();
            ServerHttpRequest newRequest = request.mutate().uri(ex).build();
            return new ServerHttpRequestDecorator(newRequest) {
                @Override
                public HttpHeaders getHeaders() {
                    HttpHeaders headers = new HttpHeaders();
                    headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
                    headers.entrySet().forEach(h -> {
                        System.out.println(h.getKey() + " | " + h.getValue());
                    });
                    return headers;
                }
    
                @SneakyThrows
                @Override
                public Flux<DataBuffer> getBody() {
                    //todo  另外一种获取body方式的实现
    //                if (null == requestBodyMap || requestBodyMap.isEmpty()) return super.getBody();
    //                String jsonBody = objectMapper.writeValueAsString(requestBodyMap);
    //                DataBuffer dataBuffer = bufferFactory.allocateBuffer();
                    return Flux.just(stringBuffer(decrypt(uuid, parameters, request.getPath().value(), response)));
                }
            };
        }
    
        /**
         * 从Flux<DataBuffer>中获取字符串的方法
         *
         * @param serverHttpRequest ServerHttpRequest
         * @return 请求体
         */
        private String resolveBodyFromRequest(ServerHttpRequest serverHttpRequest) {
            //获取请求体
            Flux<DataBuffer> body = serverHttpRequest.getBody();
            AtomicReference<String> bodyRef = new AtomicReference<>();
            body.subscribe(buffer -> {
                CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
                DataBufferUtils.release(buffer);
                bodyRef.set(charBuffer.toString());
            });
            //获取request body
            return bodyRef.get();
        }
    
        /**
         * 将字符串写入 DataBuffer
         *
         * @param value 要写入的字符串
         * @return DataBuffer
         */
        DataBuffer stringBuffer(String value) {
            byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
            NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
            DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length);
            buffer.write(bytes);
            return buffer;
        }
    
        /**
         * 解密的业务
         *
         * @param uuid       uuid
         * @param parameters body块的json字符串
         * @param urlPath    访问路由
         * @param response   ServerHttpResponse
         * @return 解密后的对象
         */
        String decrypt(String uuid, String parameters, String urlPath, ServerHttpResponse response) {
            if (!StringUtils.hasText(parameters)) {
                log.warn("当前访问 【:{}】  body 中没有信息", urlPath);
                return "";
            }
            if (!StringUtils.hasText(uuid)) {
                log.warn("当前访问 【:{}】  uuid 不存在", urlPath);
                response.setStatusCode(HttpStatus.BAD_REQUEST);
                return "";
            }
            try {
                String key = dhService.getKey(uuid);
                if (!StringUtils.hasText(key)) {
                    log.warn("当前访问 【:{}】 中没有获取到协商信息 uuid为 【:{}】", urlPath, uuid);
                    response.setStatusCode(HttpStatus.BAD_REQUEST);
                    return "";
                }
                String dParameters = AESUtil.decrypt(parameters, key);
                log.debug("当前访问 【:{}】 解密内容为 【:{}】", urlPath, dParameters);
                return dParameters;
            } catch (Exception e) {
                log.error("当前访问 【:{}】解析 body 数据发生未知异常", urlPath, e);
                response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
            }
            return "";
        }
    
        /**
         * 构造 response
         *
         * @param response ServerHttpResponse
         * @param uuid     客户端协商的唯一标记
         * @param urlPath  访问路由
         * @return 构造的 ServerHttpResponseDecorator
         */
        ServerHttpResponseDecorator processResponse(ServerHttpResponse response,
                                                    String uuid,
                                                    String urlPath) {
            DataBufferFactory bufferFactory = response.bufferFactory();
            return new ServerHttpResponseDecorator(response) {
    
                @SuppressWarnings("unchecked")
                @Override
                public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                    if (body instanceof Flux) {
                        Flux<? extends DataBuffer> flux = (Flux<? extends DataBuffer>) body;
                        return super.writeWith(flux.map(buffer -> {
                            CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
                            DataBufferUtils.release(buffer);
                            return bufferFactory.wrap(encrypt(uuid, charBuffer.toString(), urlPath).getBytes(StandardCharsets.UTF_8));
                        }));
                    }
                    return super.writeWith(body);
                }
            };
        }
    
        /**
         * 响应加密
         *
         * @param uuid    客户端协商的唯一标记
         * @param content 要加密的内容
         * @param urlPath 访问路由
         * @return 加密内容
         */
        String encrypt(String uuid, String content, String urlPath) {
            if (!StringUtils.hasText(uuid)) {
                log.warn("当前请求响应 【:{}】  uuid 不存在,不进行加密", urlPath);
                return content;
            }
            String key = dhService.getKey(uuid);
            if (!StringUtils.hasText(key)) {
                log.warn("当前访问 【:{}】 中没有获取到协商信息,不进行加密 uuid为 【:{}】", urlPath, uuid);
                return content;
            }
            return AESUtil.encrypt(content, key);
        }
    
    
        @Override
        public int getOrder() {
            //由于 response的原因 需要保证顺序在 NettyWriteResponseFilter 之前
            return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1;
        }
    }
    
    • 增加路由配置
      AuthAutoConfiguration 这里我贴出完整的配置
    import com.flashwhale.cloud.gateway.filter.EncryptionFilter;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.cloud.gateway.route.RouteLocator;
    import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import javax.ws.rs.HttpMethod;
    import java.util.Objects;
    
    /**
     * 这里进行转发路由的配置
     * 注意,如果路由需要有顺序匹配的,需要使用 order 方法 值越小优先匹配
     */
    @Slf4j
    @Configuration
    public class AuthAutoConfiguration {
        @Autowired
        EncryptionFilter encryptionFilter;
    
        @Bean
        public RouteLocator routeLocator(RouteLocatorBuilder builder) {
            return builder.routes()
                    /**
                     * 针对 cloud-gateway 提供 dh 服务的普通路由
                     */
                    .route("cloud-gateway-dh", r ->
                            r
                                    .path("/dh/**")
                                    .uri("lb://cloud-gateway")
                    )
                    /**
                     * 针对 user-server 服务的普通路由
                     */
                    .route("user-server", r ->
                            r
                                    .path("/user_api/**")
                                    .uri("lb://user-server")
                    )
                    /**
                     * 针对 user-web 服务的普通路由
                     */
                    .route("user-web", r ->
                            r
                                    .path("/user_web/**")
                                    .uri("lb://user-web").order(2)
                    )
                    /**
                     * 针对 user-web 服务需要加密的路由
                     */
                    .route("user-web-encryption", r ->
                            r
                                    .readBody(Object.class, requestBody -> true)
                                    .and()
                                    .path("/user_web/encryption/**")
                                    .and()
                                    .predicate(p -> Objects.requireNonNull(p.getRequest().getMethod()).matches(HttpMethod.POST))
                                    .filters(f -> f.filter(encryptionFilter))
                                    .uri("lb://user-web").order(1)
                    )
    
                    .build();
        }
    }
    
    说明:
    • 当一个服务需要部分路由到过滤器,其他服务正常路由的时候,类似 user-webuser-web-encryption ,两个实际上是一个路由,一个是通配,一个是特殊匹配,注意最后面的 order 方法,是必须保证特殊匹配的路由要在通配之前匹配(值越小越先匹配)
    • .readBody(Object.class, requestBody -> true) 这一行用来解决高版本的 spring cloud 出现 body 有时候取不到值的情况(对,你没有看错,就是有时候取不到值)
    • .predicate(p -> Objects.requireNonNull(p.getRequest().getMethod()).matches(HttpMethod.POST)) 用来匹配 http post 方法
    • .filters(f -> f.filter(encryptionFilter)) 这就是一个过滤器的配置了,当匹配到对应的请求,就会应用到过滤器,记得过滤器中也有一个 order 方法,要特别注意顺序

    到此网关基本使用完成,最后贴一下网关的关键配置:

    spring:
      cloud:
        gateway:
          discovery:
            locator:
              enabled: true #启用网关功能
              lowerCaseServiceId: true #是否忽略服务名大小写
    

    相关文章

      网友评论

          本文标题:springcloud 之 gateway

          本文链接:https://www.haomeiwen.com/subject/xdqxsktx.html