Spring Cloud gateway 网关如何拦截Post请求日志

网友投稿 333 2022-10-11


Spring Cloud gateway 网关如何拦截Post请求日志

gateway版本是 2.0.1

1.pom结构

(部分内部项目依赖已经隐藏)

org.springframework.cloud

spring-cloud-starter-netflix-eureka-client

org.springframework.cloud

spring-cloud-starter-gateway

org.springframework.boot

spring-boot-starter-actuator

org.springframework.boot

spring-boot-starter-test

test

ch.qos.logback

logback-core

1.1.11

ch.qos.logback

logback-classic

1.1.11

org.apache.httpcomponents

httpclient

4.5.6

org.crazycake

jdbctemplatetool

1.0.4-RELEASE

mysql

mysql-connector-java

com.alibaba

druid

2.表结构

CREATE TABLE `zc_log_notes` (

`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '日志信息记录表主键id',

`notes` varchar(255) DEFAULT NULL COMMENT '操作记录信息',

`amenu` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '一级菜单',

`bmenu` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '二级菜单',

`ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '操作人ip地址,先用varchar存',

`params` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '请求值',

`response` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '返回值',

`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',

`create_user` int(11) DEFAULT NULL COMMENT '操作人id',

`end_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '响应时间',

`status` int(1) NOT NULL DEFAULT '1' COMMENT '响应结果1成功0失败',

PRIMARY KEY (`id`)

) ENGINE=InnoDB AUTO_INCREMENT=103 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='日志信息记录表';

3.实体结构

@Table(catalog = "zhiche", name = "zc_log_notes")

public class LogNotes {

/**

* 日志信息记录表主键id

*/

private Integer id;

/**

* 操作记录信息

*/

private String notes;

/**

* 一级菜单

*/

private String amenu;

/**

* 二级菜单

*/

private String bmenu;

/**

* 操作人ip地址,先用varchar存

*/

private String ip;

/**

* 请求参数记录

*/

private String params;

/**

* 返回结果记录

*/

private String response;

/**

* 操作时间

*/

private Date createTime;

/**

* 操作人id

*/

private Integer createUser;

/**

* 响应时间

*/

private Date endTime;

/**

* 响应结果1成功0失败

*/

private Integer status;

@Id

@GeneratedValue(strategy = GenerationType.IDENTITY)

public Integer getId() {

return id;

}

public void setId(Integer id) {

this.id = id;

}

public String getNotes() {

return notes;

}

public void setNotes(String notes) {

this.notes = notes;

}

public String getAmenu() {

return amenu;

}

public void setAmenu(String amenu) {

this.amenu = amenu;

}

public String getBmenu() {

return bmenu;

}

public void setBmenu(String bmenu) {

this.bmenu = bmenu;

}

public String getIp() {

return ip;

}

public void setIp(String ip) {

this.ip = ip;

}

public Date getCreateTime() {

return createTime;

}

public void setCreateTime(Date createTime) {

this.createTime = createTime;

}

public Integer getCreateUser() {

return createUser;

}

public void setCreateUser(Integer createUser) {

this.createUser = createUser;

}

public Date getEndTime() {

return endTime;

}

public void setEndTime(Date endTime) {

this.endTime = endTime;

}

public Integer getStatus() {

return status;

}

public void setStatus(Integer status) {

this.status = status;

}

public String getParams() {

return params;

}

public void setParams(String params) {

this.params = params;

}

public String getResponse() {

return response;

}

public void setResponse(String response) {

this.response = response;

}

public void setAppendResponse(String response){

if (StringUtils.isNoneBlank(this.response)) {

this.response = this.response + response;

} else {

this.response = response;

}

}

}

4.dao层和Service层省略..

5.filter代码

1. RequestRecorderGlobalFilter 实现了GlobalFilter和Order

package com.zc.gateway.filter;

import com.zc.entity.LogNotes;

import com.zc.gateway.service.FilterService;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.cloud.gateway.filter.GatewayFilterChain;

import org.springframework.cloud.gateway.filter.GlobalFilter;

import org.springframework.core.Ordered;

import org.springframework.core.io.buffer.DataBuffer;

import org.springframework.core.io.buffer.DataBufferUtils;

import org.springframework.http.HttpHeaders;

import org.springframework.http.HttpMethod;

import org.springframework.http.HttpStatus;

import org.springframework.http.MediaType;

import org.springframework.http.server.reactive.ServerHttpRequest;

import org.springframework.http.server.reactive.ServerHttpResponse;

import org.springframework.lang.Nullable;

import org.springframework.stereotype.Component;

import org.springframework.web.server.ServerWebExchange;

import reactor.core.publisher.Flux;

import reactor.core.publisher.Mono;

import java.net.URI;

import java.nio.CharBuffer;

import java.nio.charset.Charset;

import java.nio.charset.StandardCharsets;

/**

* @author qiwenshuai

* @note 目前只记录了request方式为POST请求的方式

* @since 19-5-16 17:29 by jdk 1.8

*/

@Component

public class RequestRecorderGlobalFilter implements GlobalFilter, Ordered {

@Autowired

FilterService filterService;

private Logger logger = LoggerFactory.getLogger(RequestRecorderGlobalFilter.class);

@Override

public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {

ServerHttpRequest originalRequest = exchange.getRequest();

URI originalRequestUrl = originalRequest.getURI();

//只记录http的请求

String scheme = originalRequestUrl.getScheme();

if ((!"http".equals(scheme) && !"https".equals(scheme))) {

return chain.filter(exchange);

}

//这是我要打印的log-StringBuilder

StringBuilder logbuilder = new StringBuilder();

//我自己的log实体

LogNotes logNotes = new LogNotes();

// 返回解码

RecorderServerHttpResponseDecorator response = new RecorderServerHttpResponseDecorator(exchange.getResponse(), logNotes, filterService);

//请求解码

RecorderServerHttpRequestDecorator recorderServerHttpRequestDecorator = new RecorderServerHttpRequestDecorator(exchange.getRequest());

//增加过滤拦截吧

ServerWebExchange ex = exchange.mutate()

.request(recorderServerHttpRequestDecorator)

.response(response)

.build();

// 观察者模式 打印一下请求log

// 这里可以在 配置文件中我进行配置

// if (logger.isDebugEnabled()) {

response.beforeCommit(() -> Mono.defer(() -> printLog(logbuilder, response)));

// }

return recorderOriginalRequest(logbuilder, ex, logNotes)

.then(chain.filter(ex))

.then();

}

private Mono recorderOriginalRequest(StringBuilder logBuffer, ServerWebExchange exchange, LogNotes logNotes) {

logBuffer.append(System.currentTimeMillis())

.append("------------");

ServerHttpRequest request = exchange.getRequest();

Mono result = recorderRequest(request, logBuffer.append("\n原始请求:\n"), logNotes);

try {

filterService.addLog(logNotes);

} catch (Exception e) {

logger.error("保存请求参数出现错误, e->{}", e.getMessage());

}

return result;

}

/**

* 记录原始请求逻辑

*/

private Mono recorderRequest(ServerHttpRequest request, StringBuilder logBuffer, LogNotes logNotes) {

URI uri = request.getURI();

HttpMethod method = request.getMethod();

HttpHeaders headers = request.getHeaders();

logNotes.setIp(headers.getHost().getHostString());

logNotes.setAmenu("一级菜单");

logNotes.setBmenu("二级菜单");

logNotes.setNotes("操作记录");

logBuffer

.append(method.toString()).append(' ')

.append(uri.toString()).append('\n');

logBuffer.append("------------请求头------------\n");

headers.forEach((name, values) -> {

values.forEach(value -> {

logBuffer.append(name).append(":").append(value).append('\n');

});

});

Charset bodyCharset = null;

if (hasBody(method)) {

long length = headers.getContentLength();

if (length <= 0) {

logBuffer.append("------------无body------------\n");

} else {

logBuffer.append("------------body 长度:").append(length).append(" contentType:");

MediaType contentType = headers.getContentType();

if (contentType == null) {

logBuffer.append("null,不记录body------------\n");

} else if (!shouldRecordBody(contentType)) {

logBuffer.append(contentType.toString()).append(",不记录body------------\n");

} else {

bodyCharset = getMediaTypeCharset(contentType);

logBuffer.append(contentType.toString()).append("------------\n");

}

}

}

if (bodyCharset != null) {

return doRecordReqBody(logBuffer, request.getBody(), bodyCharset, logNotes)

.then(Mono.defer(() -> {

logBuffer.append("\n------------ end ------------\n\n");

return Mono.empty();

}));

} else {

logBuffer.append("------------ end ------------\n\n");

return Mono.empty();

}

}

//日志输出返回值

private Mono printLog(StringBuilder logBuilder, ServerHttpResponse response) {

HttpStatus statusCode = response.getStatusCode();

assert statusCode != null;

logBuilder.append("响应:").append(statusCode.value()).append(" ").append(statusCode.getReasonPhrase()).append('\n');

HttpHeaders headers = response.getHeaders();

logBuilder.append("------------响应头------------\n");

headers.forEach((name, values) -> {

values.forEach(value -> {

logBuilder.append(name).append(":").append(value).append('\n');

});

});

logBuilder.append("\n------------ end at ")

.append(System.currentTimeMillis())

.append("------------\n\n");

logger.info(logBuilder.toString());

return Mono.empty();

}

//

@Override

public int getOrder() {

//在GatewayFilter之前执行

return -1;

}

private boolean hasBody(HttpMeXvYGvhkJNethod method) {

//只记录这3种谓词的body

// if (method == HttpMethod.POST || method == HttpMethod.PUT || method == HttpMethod.PATCH)

return true;

// return false;

}

//记录简单的常见的文本类型的request的body和response的body

private boolean shouldRecordBody(MediaType contentType) {

String type = contentType.getType();

String subType = contentType.getSubtype();

if ("application".equals(type)) {

return "json".equals(subType) || "x-www-form-urlencoded".equals(subType) || "xml".equals(subType) || "atom+xml".equals(subType) || "rss+xml".equals(subType);

} else if ("text".equals(type)) {

return true;

}

//暂时不记录form

return false;

}

// 获取请求的参数

private Mono doRecordReqBody(StringBuilder logBuffer, Flux body, Charset charset, LogNotes logNotes) {

return DataBufferUtils.join(body).doOnNext(buffer -> {

CharBuffer charBuffer = charset.decode(buffer.asByteBuffer());

//记录我实体的请求体

logNotes.setParams(charBuffer.toString());

logBuffer.append(charBuffer.toString());

DataBufferUtils.release(buffer);

}).then();

}

private Charset getMediaTypeCharset(@Nullable MediaType mediaType) {

if (mediaType != null && mediaType.getCharset() != null) {

return mediaType.getCharset();

} else {

return StandardCharsets.UTF_8;

}

}

}

2.RecorderServerHttpRequestDecorator 继承了ServerHttpRequestDecorator

package com.zc.gateway.filter;

import com.zc.entity.LogNotes;

import org.springframework.core.io.buffer.DataBuffer;

import org.springframework.http.server.reactive.ServerHttpRequest;

import org.springframework.http.server.reactive.ServerHttpRequestDecorator;

import reactor.core.publisher.Flux;

import reactor.core.publisher.Mono;

import java.util.LinkedList;

import java.util.List;

/**

* @author qiwenshuai

* @note

* @since 19-5-16 17:30 by jdk 1.8

*/

// request

public class RecorderServerHttpRequestDecorator extends ServerHttpRequestDecorator {

private final List dataBuffers = new LinkedList<>();

private boolean bufferCached = false;

private Mono progress = null;

public RecorderServerHttpRequestDecorator(ServerHttpRequest delegate) {

super(delegate);

}

//重写request请求体

@Override

public Flux getBody() {

synchronized (dataBuffers) {

if (bufferCached)

return copy();

if (progress == null) {

progress = cache();

}

return progress.thenMany(Flux.defer(this::copy));

}

}

private Flux copy() {

return Flux.fromIterable(dataBuffers)

.map(buf -> buf.factory().wrap(buf.asByteBuffer()));

}

private Mono cache() {

return super.getBody()

.map(dataBuffers::add)

.then(Mono.defer(()-> {

bufferCached = true;

progress = null;

return Mono.empty();

}));

}

}

3.RecorderServerHttpResponseDecorator 继承了 ServerHttpResponseDecorator

package com.zc.gateway.filter;

import com.zc.entity.LogNotes;

import com.zc.gateway.service.FilterService;

import org.reactivestreams.Publisher;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.core.io.buffer.DataBufferFactory;

import org.springframework.core.io.buffer.DataBufferUtils;

import org.springframework.http.server.reactive.ServerHttpResponse;

import org.springframework.http.server.reactive.ServerHttpResponseDecorator;

import reactor.core.publisher.Flux;

import reactor.core.publisher.Mono;

import org.eXvYGvhkJNspringframework.core.io.buffer.DataBuffer;

import java.nio.charset.Charset;

import java.util.LinkedList;

import java.util.List;

/**

* @author qiwenshuai

* @note

* @since 19-5-16 17:32 by jdk 1.8

*/

public class RecorderServerHttpResponseDecorator extends ServerHttpResponseDecorator {

private Logger logger = LoggerFactory.getLogger(RecorderServerHttpResponseDecorator.class);

private LogNotes logNotes;

private FilterService filterService;

RecorderServerHttpResponseDecorator(ServerHttpResponse delegate, LogNotes logNotes, FilterService filterService) {

super(delegate);

this.logNotes = logNotes;

this.filterService = filterService;

}

/**

* 基于netty,我这里需要显示的释放一次dataBuffer,但是slice出来的byte是不需要释放的,

* 与下层共享一个字符串缓冲池,gateway过滤器使用的是nettyWrite类,会发生response数据多次才能返回完全。

* 在 ServerHttpResponseDecorator 之后会释放掉另外一个refCount.

*/

@Override

public Mono writeWith(Publisher extends DataBuffer> body) {

DataBufferFactory bufferFactory = this.bufferFactory();

if (body instanceof Flux) {

Flux extends DataBuffer> fluxBody = (Flux extends DataBuffer>) body;

Publisher extends DataBuffer> re = fluxBody.map(dataBuffer -> {

// probably should reuse buffers

byte[] content = new byte[dataBuffer.readableByteCount()];

// 数据读入数组

dataBuffer.read(content);

// 释放掉内存

DataBufferUtils.release(dataBuffer);

// 记录返回值

String s = new String(content, Charset.forName("UTF-8"));

logNotes.setAppendResponse(s);

try {

filterService.updateLog(logNotes);

} catch (Exception e) {

logger.error("Response值修改日志记录出现错误->{}", e);

}

byte[] uppedContent = new String(content, Charset.forName("UTF-8")).getBytes();

return bufferFactory.wrap(uppedContent);

});

return super.writeWith(re);

}

return super.writeWith(body);

}

@Override

public Mono writeAndFlushWith(Publisher extends Publisher extends DataBuffer>> body) {

return writeWith(Flux.from(body).flatMapSequential(p -> p));

}

}

注意:

网关过滤返回值 底层用到了Netty服务,在response返回的时候,有时候会写的数据是不全的,于是我在实体类中新增了一个setAppendResponse方法进行拼接, 再者,gateway的过滤器是链式结构,需要定义order排序为最先(-1),然后和预置的gateway过滤器做一个combine.

代码中用到的 dataBuffer 结构,底层其实也是类似netty的byteBuffer,用到了字节数组池,同时也用到了 引用计数器 (refInt).

为了让jvm在gc的时候垃圾得到回收,避免内存泄露,我们需要在转换字节使用的地方,显示的释放一次

DataBufferUtils.release(dataBuffer);


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:汉源高科4光6电千兆环网光交换机卡轨式网管型光纤冗余自愈环网工业以太网交换机
下一篇:Cookie和Session使用(cookie和session使用场景)
相关文章

 发表评论

暂时没有评论,来抢沙发吧~