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

网友投稿 970 2022-12-27

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

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-.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小时内删除侵权内容。

上一篇:flutter优秀作品(flutter设计)
下一篇:广州一体化政务服务平台(广州政务服务网站)
相关文章

 发表评论

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