打印需求:
? ? ? ? 對所有的接口打印:請求方式,請求路徑,請求參數,用戶id,訪問IP,訪問時間
? ? ? ? 對增刪改操作的接口打印:接口響應
打印方案:
? ? ? ? 給GET設置一個白名單(因為get請求大多數是查詢,僅有部分增刪改操作),在這個白名單里的接口,需要打印響應,不在的話就只打印一下基礎信息
? ? ? ? 給POST請求設置一個黑名單(因為post請求大多數會對數據進行操作,僅有小部分的查詢),在這個黑名單里的查詢接口,則不需要打印接口響應
?實現方式:
? ? ? ? 在過濾器中進行操作
package com.xxxxxx.gateway.filter;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.serializer.ValueFilter;
import com.koushare.common.constant.AuthConstants;
import com.koushare.common.constant.TokenConstants;
import com.koushare.common.utils.IPUtils;
import com.koushare.common.utils.StringUtils;
import com.koushare.gateway.model.CheckRequest;
import com.koushare.gateway.service.SysPrintLogService;
import com.koushare.jwt.config.PassJavaJwtProperties;
import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.core.Ordered;
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.DefaultDataBufferFactory;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;import javax.annotation.Resource;import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;import static com.koushare.common.convertor.ConvertHelper.bean2map;
import static com.koushare.common.utils.IPUtils.getIpAddrByServerHttpRequest;/*** 自定義全局過濾器*/
@Component
@Slf4j
public class GlobalRequestFilter implements GlobalFilter, Ordered {@Resourceprivate PassJavaJwtProperties jwtProperties;@Autowiredprivate SysPrintLogService sysPrintLogService;private static final String OPENAPI_SERVICE = "/openapi/";private static final String OPENAPI_CODE_PATH = "/code/";@Overridepublic int getOrder() {return -20;}@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {ServerHttpRequest serverHttpRequest = exchange.getRequest();// 獲取原始響應對象和數據緩沖工廠ServerHttpResponse originalResponse = exchange.getResponse();DataBufferFactory bufferFactory = originalResponse.bufferFactory();// 原始響應對象,用于攔截和修改響應內容ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {/*** 重寫writeWith方法攔截響應體*/@Overridepublic Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {if (body instanceof Flux) {Flux<? extends DataBuffer> fluxBody = Flux.from(body);return super.writeWith(fluxBody.buffer().map(dataBuffers -> {// 獲取一些需要打印的參數long timestamp = System.currentTimeMillis();HttpMethod method = serverHttpRequest.getMethod();String requestUrl = serverHttpRequest.getPath().toString();String userId = Optional.ofNullable(serverHttpRequest.getHeaders().getFirst(AuthConstants.USER_ID)).filter(StringUtils::isNotBlank).orElse("未登錄");String ip = IPUtils.getIpAddrByServerHttpRequest(serverHttpRequest);String params = getRequestparams(serverHttpRequest, exchange);log.info("{} ========================接口詳細日志========================", timestamp);log.info("{} 請求方式:{} 請求路徑: {}", timestamp, method, requestUrl);log.info("{} 請求參數: {}", timestamp, params);log.info("{} 用戶ID: {} 訪問IP: {} 訪問時間:{}", timestamp, userId, ip, new Date());// 判斷是否需要打印響應if (isUpdateDate(method, requestUrl)) {// 創建數據緩沖工廠和緩沖區,用于讀取響應內容DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();DataBuffer buff = dataBufferFactory.join(dataBuffers);byte[] content = new byte[buff.readableByteCount()];buff.read(content);// 釋放緩沖區資源DataBufferUtils.release(buff);// 獲取響應內容類型MediaType contentType = originalResponse.getHeaders().getContentType();if (!MediaType.APPLICATION_JSON.isCompatibleWith(contentType)) {// 如果不是JSON類型,直接返回原始內容,不進行處理log.info("{} ===============================================================", timestamp);return bufferFactory.wrap(content);}// 將字節數組轉換為字符串 對響應體進行統一格式化處理String result = modifyBody(new String(content));log.info("{} 響應結果: {}", timestamp, result);log.info("{} ===============================================================", timestamp);getDelegate().getHeaders().setContentLength(result.getBytes().length);return bufferFactory.wrap(result.getBytes());} else {// 不需要打印響應結果時,直接合并并返回原始數據log.info("{} ===============================================================", timestamp);DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();DataBuffer joinedBuffer = dataBufferFactory.join(dataBuffers);byte[] content = new byte[joinedBuffer.readableByteCount()];joinedBuffer.read(content);DataBufferUtils.release(joinedBuffer);return bufferFactory.wrap(content);}}));} else {return super.writeWith(body);}}};return chain.filter(exchange.mutate().response(decoratedResponse).build());}private static String getRouteName(String requestUrl) {String serviceUrl = requestUrl.substring(requestUrl.indexOf("/") + 1);log.info("getRouteName: " + serviceUrl.substring(0, serviceUrl.indexOf("/")));return serviceUrl.substring(0, serviceUrl.indexOf("/"));}/*** 獲取請求token*/private String getToken(ServerHttpRequest request) {String token = request.getHeaders().getFirst(jwtProperties.getHeader());// 如果前端設置了令牌前綴,則裁剪掉前綴if (StringUtils.isNotEmpty(token) && token.startsWith(TokenConstants.PREFIX)) {token = token.replaceFirst(TokenConstants.PREFIX, StringUtils.EMPTY);}return token;}/*** 獲取去除路由后的path** @param requestUrl* @return*/private static String getPath(String requestUrl) {String path = requestUrl.substring(1);log.info("getPath: " + path.substring(path.indexOf("/")));return path.substring(path.indexOf("/"));}/*** 判斷是否為增刪改接口 只有增刪改接口才會打印響應結果*/private boolean isUpdateDate(HttpMethod method, String requestUrl){switch (method) {case PUT:case DELETE:return true;case GET:return sysPrintLogService.checkNeedPrint_GET(requestUrl);case POST:return sysPrintLogService.checkNeedPrint_POST(requestUrl);default:return false;}}/*** 獲取請求參數*/private String getRequestparams(ServerHttpRequest serverHttpRequest, ServerWebExchange exchange) {HttpMethod method = serverHttpRequest.getMethod();// 檢查是否為文件上傳請求,如果是則不打印參數MediaType contentType = serverHttpRequest.getHeaders().getContentType();if (contentType != null && (contentType.includes(MediaType.MULTIPART_FORM_DATA)|| contentType.includes(MediaType.APPLICATION_OCTET_STREAM))) {return "";}if (HttpMethod.GET.equals(method) || HttpMethod.DELETE.equals(method)) {StringBuilder params = new StringBuilder();serverHttpRequest.getQueryParams().forEach((key, value) -> {value.forEach(v -> params.append(key).append("=").append(v).append("&"));});// 移除末尾的 "&"if (params.length() > 0) {params.deleteCharAt(params.length() - 1);}return params.toString();} else if (HttpMethod.POST.equals(method) || HttpMethod.PUT.equals(method)) {return getBodyContent(exchange);}return "";}// 從其他filter中copy過來的 目的是獲取post請求的bodyprivate String getBodyContent(ServerWebExchange exchange){Flux<DataBuffer> body = exchange.getRequest().getBody();AtomicReference<String> bodyRef = new AtomicReference<>();// 緩存讀取的request body信息body.subscribe(dataBuffer -> {CharBuffer charBuffer = StandardCharsets.UTF_8.decode(dataBuffer.asByteBuffer());DataBufferUtils.release(dataBuffer);bodyRef.set(charBuffer.toString());});//獲取request bodyreturn bodyRef.get();}/*** 修改響應體內容,統一JSON數據格式*/private String modifyBody(String str){JSONObject json = JSON.parseObject(str, Feature.AllowISO8601DateFormat);JSONObject.DEFFAULT_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";return JSONObject.toJSONString(json, (ValueFilter) (object, name, value) ->value == null ? "" : value, SerializerFeature.WriteDateUseDateFormat);}}
其中的service實現類:
package com.xxxxxx.gateway.service.impl;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.koushare.gateway.entity.SysPrintLog;
import com.koushare.gateway.mapper.SysPrintLogMapper;
import com.koushare.gateway.service.SysPrintLogService;
import com.koushare.redis.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.List;@Slf4j
@Service
public class SysPrintLogServiceImpl implements SysPrintLogService {private final static String PrintLogUrlWhitelist_GET = "PrintLog:UrlWhiteList";private final static String PrintLogUrlBlacklist_POST = "PrintLog:UrlBlackList";@Autowiredprivate SysPrintLogMapper sysPrintLogMapper;@Autowiredprivate RedisUtils redisUtils;/*** 檢查Get請求是否需要打印* @return true:需要打印; false:不需要打印*/@Overridepublic boolean checkNeedPrint_GET(String requestUrl) {return checkWhiteList(requestUrl);}/*** 檢查Post請求是否需要打印* @return true:需要打印; false:不需要打印*/@Overridepublic boolean checkNeedPrint_POST(String requestUrl) {return checkBlackList(requestUrl);}/*** 重新加載redis的Get請求日志打印接口白名單*/@Overridepublic List<SysPrintLog> loadPringLogWhiteList() {List<SysPrintLog> list = sysPrintLogMapper.queryPrintLogWhiteList();JSONArray jsonArray = JSONArray.parseArray(JSON.toJSONString(list));redisUtils.set(PrintLogUrlWhitelist_GET, jsonArray);return list;}/*** 重新加載redis的Post請求日志打印接口黑名單*/@Overridepublic List<SysPrintLog> loadPrintLogBlackList() {List<SysPrintLog> list = sysPrintLogMapper.queryPrintLogBlackList();JSONArray jsonArray = JSONArray.parseArray(JSON.toJSONString(list));redisUtils.set(PrintLogUrlBlacklist_POST, jsonArray);return list;}/*** 讀redis中的白名單,不存在則讀取數據庫*/private List<SysPrintLog> WhiteList(){JSONArray jsonArray = redisUtils.getJsonArray(PrintLogUrlWhitelist_GET);if (jsonArray == null || jsonArray.size() == 0) {return loadPringLogWhiteList();}return jsonArray.toJavaList(SysPrintLog.class);}/*** 讀redis中的黑名單,不存在則讀取數據庫*/private List<SysPrintLog> BlackList(){JSONArray jsonArray = redisUtils.getJsonArray(PrintLogUrlBlacklist_POST);if (jsonArray == null || jsonArray.size() == 0) {return loadPrintLogBlackList();}return jsonArray.toJavaList(SysPrintLog.class);}/*** 白名單列表中查找是否存在匹配的URL*/private boolean checkWhiteList(String requestUrl){return WhiteList().stream().anyMatch(sysPrintLog -> sysPrintLog.getUrl().equals(requestUrl));}/*** 黑名單列表中查找是否存在匹配的URL*/private boolean checkBlackList(String requestUrl){return BlackList().stream().noneMatch(sysPrintLog -> sysPrintLog.getUrl().equals(requestUrl));}}
用到的兩個查詢:
@Mapper
public interface SysPrintLogMapper {@Select("select id,url,description from sys_print_log_whitelist")List<SysPrintLog> queryPrintLogWhiteList();@Select("select id,url,description from sys_print_log_blacklist")List<SysPrintLog> queryPrintLogBlackList();}
以及數據庫實體
@Data
@Builder(setterPrefix = "set")
@NoArgsConstructor
@AllArgsConstructor
public class SysPrintLog {private Integer id;private String url;private String description;}
獲取ip地址的工具類:
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.server.reactive.ServerHttpRequest;import javax.servlet.http.HttpServletRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;/*** IP地址** @author zhanghai on 2018/9/17*/
@Slf4j
public class IPUtils {// 多次反向代理后會有多個ip值 的分割符private final static String IP_UTILS_FLAG = ",";// 未知IPprivate final static String UNKNOWN = "unknown";// 本地 IPprivate final static String LOCALHOST_IP = "0:0:0:0:0:0:0:1";private final static String LOCALHOST_IP1 = "127.0.0.1";/*** 獲取IP地址* 使用Nginx等反向代理軟件, 則不能通過request.getRemoteAddr()獲取IP地址* 如果使用了多級反向代理的話,X-Forwarded-For的值并不止一個,而是一串IP地址,X-Forwarded-For中第一個非unknown的有效IP字符串,則為真實IP地址*/public static String getIpAddr(HttpServletRequest request) {String ip = null;try {ip = request.getHeader("x-forwarded-for");if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {ip = request.getHeader("Proxy-Client-IP");}if (StringUtils.isEmpty(ip) || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {ip = request.getHeader("WL-Proxy-Client-IP");}if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {ip = request.getHeader("HTTP_CLIENT_IP");}if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {ip = request.getHeader("HTTP_X_FORWARDED_FOR");}if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {ip = request.getRemoteAddr();}} catch (Exception e) {log.error("IPUtils ERROR ", e);}// 使用代理,則獲取第一個IP地址if (StringUtils.isEmpty(ip) && ip.length() > 15) {if (ip.indexOf(",") > 0) {ip = ip.substring(0, ip.indexOf(","));}}return ip;}public static String getIpAddrByServerHttpRequest(ServerHttpRequest request) {// 根據 HttpHeaders 獲取 請求 IP地址String ip = request.getHeaders().getFirst("X-Forwarded-For");if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {ip = request.getHeaders().getFirst("x-forwarded-for");if (ip != null && ip.length() != 0 && !UNKNOWN.equalsIgnoreCase(ip)) {// 多次反向代理后會有多個ip值,第一個ip才是真實ipif (ip.contains(IP_UTILS_FLAG)) {ip = ip.split(IP_UTILS_FLAG)[0];}}}if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {ip = request.getHeaders().getFirst("Proxy-Client-IP");}if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {ip = request.getHeaders().getFirst("WL-Proxy-Client-IP");}if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {ip = request.getHeaders().getFirst("HTTP_CLIENT_IP");}if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {ip = request.getHeaders().getFirst("HTTP_X_FORWARDED_FOR");}if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {ip = request.getHeaders().getFirst("X-Real-IP");}//兼容k8s集群獲取ipif (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {ip = request.getRemoteAddress().getAddress().getHostAddress();if (LOCALHOST_IP1.equalsIgnoreCase(ip) || LOCALHOST_IP.equalsIgnoreCase(ip)) {//根據網卡取本機配置的IPInetAddress iNet = null;try {iNet = InetAddress.getLocalHost();} catch (UnknownHostException e) {log.error("getClientIp error: ", e);}ip = iNet.getHostAddress();}}return ip;}/*** 獲取IP地址* 使用Nginx等反向代理軟件, 則不能通過request.getRemoteAddr()獲取IP地址* 如果使用了多級反向代理的話,X-Forwarded-For的值并不止一個,而是一串IP地址,X-Forwarded-For中第一個非unknown的有效IP字符串,則為真實IP地址*/public static String getIpAddrByHttp(ServerHttpRequest request) {String ip = null;try {ip = request.getHeaders().getFirst("x-forwarded-for");if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {ip = request.getHeaders().getFirst("Proxy-Client-IP");}if (StringUtils.isEmpty(ip) || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {ip = request.getHeaders().getFirst("WL-Proxy-Client-IP");}if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {ip = request.getHeaders().getFirst("HTTP_CLIENT_IP");}if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {ip = request.getHeaders().getFirst("HTTP_X_FORWARDED_FOR");}if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {ip = request.getHeaders().getFirst("X-Real-IP");}} catch (Exception e) {log.error("IPUtils ERROR ", e);}// 使用代理,則獲取第一個IP地址if (StringUtils.isEmpty(ip) && ip.length() > 15) {if (ip.indexOf(",") > 0) {ip = ip.substring(0, ip.indexOf(","));}}return ip;}}