天天看點

gateway配置_springcloud 之 gateway

值得一提的是低版本的

springcloud

網關是

zuul

springboot 2.x

新版本的網關是

gateway

通過本文你可以學到2點:

  • 如何在網關層重寫

    http

    請求與響應
  • 如何通過代碼的方式配置路由

一、網關的引入用來解決什麼問題

gateway

主要做2件事:

  • 路由
  • 過濾

二、如何使用路由

配置檔案的方式使用路由的例子很多,這裡我不再贅述,這裡我隻打算寫一下通過代碼完成路由配置

AuthAutoConfiguration

建立一個配置檔案

說明:
  • 一個

    .route

    就是一個路由規則
  • 路由中的

    id

    是唯一的,不一定需要與服務名對應,不重複即可
  • path

    方法比對是根據路徑來的,實際上是一個通配,是以這裡需要在對應的服務前加上字首,類似這樣:
  • uri

    方法是必須的,必須聲明一個位址,通過

    lb://服務名

    可以使用到

    eureka

    負載均衡的轉發特性,當然也可以使用類似

    http://localhost:8080

    等方式

二、如何使用過濾

同樣的配置檔案的方式使用過濾的例子很多,這裡我不再贅述,這裡我隻打算寫一下通過代碼完成過濾配置
  • EncryptionFilter

    編寫一個網關過濾器,這裡不是全局的,目前示範的過濾器是修改請求和響應體完成加密解密的過程,有關

    DH

    的部分請參考 dh秘鑰交換算法實踐[1] 如果這篇文章不可見,請移步我的公衆号檢視,簡書的規則不知道是什麼,就鎖定了這篇文章。
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 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 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 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中擷取字元串的方法
     *
     * @param serverHttpRequest ServerHttpRequest
     * @return 請求體
     */
    private String resolveBodyFromRequest(ServerHttpRequest serverHttpRequest) {
        //擷取請求體
        Flux body = serverHttpRequest.getBody();
        AtomicReference 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 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

    這裡我貼出完整的配置
說明:
  • 當一個服務需要部分路由到過濾器,其他服務正常路由的時候,類似

    user-web

    user-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

    方法,要特别注意順序

到此網關基本使用完成,最後貼一下網關的關鍵配置:

gateway配置_springcloud 之 gateway

歡迎關注我的個人公衆号

參考資料

[1]

dh秘鑰交換算法實踐: https://www.jianshu.com/p/2c45126f64a1