Spring Cloud Alibaba实战(六) - Gateway之鉴权、日志

时间:2024-11-06 12:23:56

目录

(一)Nacos动态配置
(二)Nacos注册中心
(三)Sentinel之限流
(四)Sentinel之熔断
(五)Gateway之路由、限流
(六)Gateway之鉴权、日志
(七)Gateway搭配Nacos实现动态路由
(八)Dubbo + Nacos

正文

在引入网关后,通常会把每个服务都要做的工作,诸如日志、安全验证等转移到网关处理以减少重复开发。

1 加入log4j2

这里使用log4j2作为日志组件,首先添加log4j2的依赖并排除SpringBoot默认日志组件的依赖

        <dependency>
            <groupId></groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
        </dependency>

        <dependency>
            <groupId></groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <exclusions>
                <exclusion>
                    <groupId></groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

在resources目录下创建

<?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:} [%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}-%">
            <PatternLayout
                    pattern="%date{yyyy-MM-dd HH:mm:} %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}-%">
            <PatternLayout
                    pattern="%date{yyyy-MM-dd HH:mm:} %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="">
            <AppenderRef ref="Console" />
        </Logger>
    </Loggers>
</Configuration>

在中增加配置告知log4j2文件路径

logging:
  config: classpath:

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 = ();
        String method = ();
        if("POST".equalsIgnoreCase(method)) {
            ServerRequest serverRequest = new DefaultServerRequest(exchange);
            Mono<String> bodyToMono = ();
            return (body -> {
                ().put("cachedRequestBody", body);
                ServerHttpRequest newRequest = new ServerHttpRequestDecorator(serverHttpRequest) {
                    @Override
                    public HttpHeaders getHeaders() {
                        HttpHeaders httpHeaders = new HttpHeaders();
                        (());
                        (HttpHeaders.TRANSFER_ENCODING, "chunked");
                        return httpHeaders;
                    }

                    @Override
                    public Flux<DataBuffer> getBody() {
                        NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(new UnpooledByteBufAllocator(false));
                        DataBuffer bodyDataBuffer = (());
                        return (bodyDataBuffer);
                    }
                };
                return (().request(newRequest).build());
            });
        }
        return (exchange);
    }

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

3 记录日志

接下来再创建一个过滤器用于记录日志

 

@Component
public class LogFilter implements GlobalFilter, Ordered {

    static final Logger logger = ("request");

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        StringBuilder logBuilder = new StringBuilder();
        ServerHttpRequest serverHttpRequest = ();
        String method = ().toUpperCase();
        (method).append(",").append(());
        if("POST".equals(method)) {
            String body = ("cachedRequestBody", "");
            if((body)) {
                (",body=").append(body);
            }
        }

        ServerHttpResponse serverHttpResponse = ();
        DataBufferFactory 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 ((dataBuffer -> {
                        byte[] content = new byte[()];
                        (content);
                        (dataBuffer);
                        String resp = new String(content, ("UTF-8"));
                        (",resp=").append(resp);
                        (());
                        byte[] uppedContent = new String(content, ("UTF-8")).getBytes();
                        return (uppedContent);
                    }));
                }
                return (body);
            }
        };
        return (().response(decoratedResponse).build());
    }

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

4 鉴权

对请求的安全验证方案视各自项目需求而定,没有固定的做法,这里仅演示检查签名的处理。规则是:对除sign外所有请求参数按字典顺序排序后组成key1=value1&key2=value2的字符串,然后计算MD5码并与sign参数值比较,一致即认为通过。

这里面同样要处理QueryString和POST方法的Body,因此和日志过滤器合并为在一起。

@Component
public class AuthAndLogFilter implements GlobalFilter, Ordered {

    static final Logger logger = ("request");

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        ServerHttpRequest serverHttpRequest = ();
        ServerHttpResponse serverHttpResponse = ();

        StringBuilder logBuilder = new StringBuilder();
        Map<String, String> params = parseRequest(exchange, logBuilder);
        boolean r = checkSignature(params, serverHttpRequest);
        if(!r) {
            Map map = new HashMap<>();
            ("code", 2);
            ("message", "签名验证失败");
            String resp = (map);
            (",resp=").append(resp);
            (());
            DataBuffer bodyDataBuffer = ().wrap(());
            ().add("Content-Type", "text/plain;charset=UTF-8");
            return ((bodyDataBuffer));
        }

        DataBufferFactory 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 ((dataBuffer -> {
                        byte[] content = new byte[()];
                        (content);
                        (dataBuffer);
                        String resp = new String(content, ("UTF-8"));
                        (",resp=").append(resp);
                        (());
                        byte[] uppedContent = new String(content, ("UTF-8")).getBytes();
                        return (uppedContent);
                    }));
                }
                return (body);
            }
        };
        return (().response(decoratedResponse).build());
    }

    private Map<String, String> parseRequest(ServerWebExchange exchange, StringBuilder logBuilder) {
        ServerHttpRequest serverHttpRequest = ();
        String method = ().toUpperCase();
        (method).append(",").append(());
        MultiValueMap<String, String> query = ();
        Map<String, String> params = new HashMap<>();
        ((k, v) -> {
            (k, (0));
        });
        if("POST".equals(method)) {
            String body = ("cachedRequestBody", "");
            if((body)) {
                (",body=").append(body);
                String[] kvArray = ("&");
                for (String kv : kvArray) {
                    if (("=") >= 0) {
                        String k = ("=")[0];
                        String v = ("=")[1];
                        if(!(k)) {
                            try {
                                (k, (v, "UTF-8"));
                            } catch (UnsupportedEncodingException e) {
                            }
                        }
                    }
                }
            }
        }
        return params;
    }

    private boolean checkSignature(Map<String, String> params, ServerHttpRequest serverHttpRequest) {

        String sign = ("sign");
        if((sign)) {
            return false;
        }
        //检查签名
        Map<String, String> sorted = new TreeMap<>();
        ( (k, v) -> {
            if(!"sign".equals(k)) {
                (k, v);
            }
        });
        StringBuilder builder = new StringBuilder();
        ((k, v) -> {
            (k).append("=").append(v).append("&");
        });
        String value = ();
        value = (0, () - 1);
        if(!(MD5Utils.MD5(value))) {
            return false;
        }

        return true;
    }

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

测试

A:无签名

B:带签名GET请求

C:POST请求

本期源码

链接:/s/1Vfg9Apnl1OgL8pzeBqHYmw 
提取码:jfkl