Gateway之鑑權、日誌

原文連接:https://blog.csdn.net/autfish/article/details/90637957redis

在引入網關後,一般會把每一個服務都要作的工做,諸如日誌、安全驗證等轉移到網關處理以減小重複開發。spring

1 加入log4j2安全

這裏使用log4j2做爲日誌組件,首先添加log4j2的依賴並排除SpringBoot默認日誌組件的依賴app

  1.  
    <dependency>
  2.  
    <groupId>org.springframework.boot</groupId>
  3.  
    <artifactId>spring-boot-starter-log4j2</artifactId>
  4.  
    </dependency>
  5.  
     
  6.  
    <dependency>
  7.  
    <groupId>org.springframework.boot</groupId>
  8.  
    <artifactId>spring-boot-starter-data-redis</artifactId>
  9.  
    <exclusions>
  10.  
    <exclusion>
  11.  
    <groupId>org.springframework.boot</groupId>
  12.  
    <artifactId>spring-boot-starter-logging</artifactId>
  13.  
    </exclusion>
  14.  
    </exclusions>
  15.  
    </dependency>

在resources目錄下建立log4j2-spring.xmldom

  1.  
    <?xml version= "1.0" encoding="UTF-8"?>
  2.  
    <Configuration status= "WARN" monitorInterval="1800">
  3.  
    <properties>
  4.  
    <property name= "LOG_HOME">D:/Logs/gateway</property>
  5.  
    <property name= "REQUEST_FILE_NAME">request</property>
  6.  
    <property name= "INFO_FILE_NAME">info</property>
  7.  
    </properties>
  8.  
     
  9.  
    <Appenders>
  10.  
    <Console name= "Console" target="SYSTEM_OUT">
  11.  
    <PatternLayout pattern= "%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" />
  12.  
    </Console>
  13.  
     
  14.  
    <RollingRandomAccessFile name= "info-log"
  15.  
    fileName= "${LOG_HOME}/${INFO_FILE_NAME}.log"
  16.  
    filePattern= "${LOG_HOME}/$${date:yyyy-MM}/${INFO_FILE_NAME}-%d{yyyy-MM-dd}-%i.log">
  17.  
    <PatternLayout
  18.  
    pattern= "%date{yyyy-MM-dd HH:mm:ss.SSS} %level [%thread][%file:%line] - %msg%n"/>
  19.  
    <Policies>
  20.  
    <TimeBasedTriggeringPolicy/>
  21.  
    <SizeBasedTriggeringPolicy size= "100 MB"/>
  22.  
    </Policies>
  23.  
    <DefaultRolloverStrategy max= "100"/>
  24.  
    </RollingRandomAccessFile>
  25.  
     
  26.  
    <RollingRandomAccessFile name= "request-log"
  27.  
    fileName= "${LOG_HOME}/${REQUEST_FILE_NAME}.log"
  28.  
    filePattern= "${LOG_HOME}/$${date:yyyy-MM}/${REQUEST_FILE_NAME}-%d{yyyy-MM-dd}-%i.log">
  29.  
    <PatternLayout
  30.  
    pattern= "%date{yyyy-MM-dd HH:mm:ss.SSS} %level [%thread][%file:%line] - %msg%n"/>
  31.  
    <Policies>
  32.  
    <TimeBasedTriggeringPolicy/>
  33.  
    <SizeBasedTriggeringPolicy size= "200 MB"/>
  34.  
    </Policies>
  35.  
    <DefaultRolloverStrategy max= "200"/>
  36.  
    </RollingRandomAccessFile>
  37.  
    </Appenders>
  38.  
     
  39.  
    <Loggers>
  40.  
    <Root level= "info">
  41.  
    <AppenderRef ref= "info-log" />
  42.  
    </Root>
  43.  
    <Logger name= "request" level="info"
  44.  
    additivity= "false">
  45.  
    <AppenderRef ref= "request-log"/>
  46.  
    </Logger>
  47.  
    <Logger name= "org.springframework">
  48.  
    <AppenderRef ref= "Console" />
  49.  
    </Logger>
  50.  
    </Loggers>
  51.  
    </Configuration>

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

  1.  
    logging:
  2.  
    config: classpath:log4j2-spring.xml

2 獲取POST的Bodyspring-boot

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

CachePostBodyFilterui

@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 記錄日誌spa

接下來再建立一個過濾器用於記錄日誌

 

  1.  
    @Component
  2.  
    public class LogFilter implements GlobalFilter, Ordered {
  3.  
     
  4.  
    static final Logger logger = LogManager.getLogger("request");
  5.  
     
  6.  
    @Override
  7.  
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
  8.  
     
  9.  
    StringBuilder logBuilder = new StringBuilder();
  10.  
    ServerHttpRequest serverHttpRequest = exchange.getRequest();
  11.  
    String method = serverHttpRequest.getMethodValue().toUpperCase();
  12.  
    logBuilder.append(method).append( ",").append(serverHttpRequest.getURI());
  13.  
    if("POST".equals(method)) {
  14.  
    String body = exchange.getAttributeOrDefault( "cachedRequestBody", "");
  15.  
    if(StringUtils.isNotBlank(body)) {
  16.  
    logBuilder.append( ",body=").append(body);
  17.  
    }
  18.  
    }
  19.  
     
  20.  
    ServerHttpResponse serverHttpResponse = exchange.getResponse();
  21.  
    DataBufferFactory bufferFactory = serverHttpResponse.bufferFactory();
  22.  
    ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(serverHttpResponse) {
  23.  
    @Override
  24.  
    public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
  25.  
    if (body instanceof Flux) {
  26.  
    Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
  27.  
    return super.writeWith(fluxBody.map(dataBuffer -> {
  28.  
    byte[] content = new byte[dataBuffer.readableByteCount()];
  29.  
    dataBuffer.read(content);
  30.  
    DataBufferUtils.release(dataBuffer);
  31.  
    String resp = new String(content, Charset.forName("UTF-8"));
  32.  
    logBuilder.append( ",resp=").append(resp);
  33.  
    logger.info(logBuilder.toString());
  34.  
    byte[] uppedContent = new String(content, Charset.forName("UTF-8")).getBytes();
  35.  
    return bufferFactory.wrap(uppedContent);
  36.  
    }));
  37.  
    }
  38.  
    return super.writeWith(body);
  39.  
    }
  40.  
    };
  41.  
    return chain.filter(exchange.mutate().response(decoratedResponse).build());
  42.  
    }
  43.  
     
  44.  
    @Override
  45.  
    public int getOrder() {
  46.  
    return -20;
  47.  
    }
  48.  
    }

4 鑑權

對請求的安全驗證方案視各自項目需求而定,沒有固定的作法,這裏僅演示檢查簽名的處理。規則是:對除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請求

本期源碼

連接:https://pan.baidu.com/s/1Vfg9Apnl1OgL8pzeBqHYmw 提取碼:jfkl 

相關文章
相關標籤/搜索