SpringCloud gateway 日志打印+参数校验

背景

springcloud gateway作为流量的统一入口,我们需要实现用户校验的功能以及打印接口调用的请求地址及参数。

功能实现

打印接口调用的请求地址及参数

这个功能需要拦截所有的请求,所以我们去定义一个全局的 GlobalFilter。

@Component("LogFilter")
public class Logfilter implements GlobalFilter, Ordered {

  Logger logger = LoggerFactory.getLogger(this.getClass());


  private final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders();


  @Override
  public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    // 获取用户传来的数据类型
    MediaType mediaType = exchange.getRequest().getHeaders().getContentType();
    ServerRequest serverRequest = ServerRequest.create(exchange, messageReaders);
    // 如果是json格式,将body内容转化为object or map 都可
    if (MediaType.APPLICATION_JSON.isCompatibleWith(mediaType)) {
      Mono<Object> modifiedBody = serverRequest.bodyToMono(Object.class)
          .flatMap(body -> {
            recordLog(exchange, body);
            return Mono.just(body);
          });
      
      return getVoidMono(exchange, chain, Object.class, modifiedBody);
    }
    // 如果是表单请求
    else if (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(mediaType)) {
      Mono<String> modifiedBody = serverRequest.bodyToMono(String.class)
          // .log("modify_request_mono", Level.INFO)
          .flatMap(body -> {
            recordLog(exchange, body);
            return Mono.just(body);
          });

      return getVoidMono(exchange, chain, String.class, modifiedBody);
    }
    // 无法兼容的请求,则不读取body,像Get请求这种
    recordLog(exchange, "");
    return chain.filter(exchange.mutate().request(exchange.getRequest()).build());
  }

  /**
   * 参照 ModifyRequestBodyGatewayFilterFactory.java 截取的方法
   *
   * @param exchange
   * @param chain
   * @param outClass
   * @param modifiedBody
   * @return
   */
  private Mono<Void> getVoidMono(ServerWebExchange exchange, GatewayFilterChain chain, Class outClass,
      Mono<?> modifiedBody) {
    BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, outClass);
    HttpHeaders headers = new HttpHeaders();
    headers.putAll(exchange.getRequest().getHeaders());

    // the new content type will be computed by bodyInserter
    // and then set in the request decorator
    headers.remove(HttpHeaders.CONTENT_LENGTH);

    CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);
    return bodyInserter.insert(outputMessage, new BodyInserterContext())
        // .log("modify_request", Level.INFO)
        .then(Mono.defer(() -> {
          //由于httpRequest的body 体只能读取一次,所以需要重新构建一个httpRequest保证后续获取body不报错
          ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(
              exchange.getRequest()) {
            @Override
            public HttpHeaders getHeaders() {
              long contentLength = headers.getContentLength();
              HttpHeaders httpHeaders = new HttpHeaders();
              httpHeaders.putAll(super.getHeaders());
              if (contentLength > 0) {
                httpHeaders.setContentLength(contentLength);
              } else {
                // TODO: this causes a 'HTTP/1.1 411 Length Required' on httpbin.org
                httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
              }
              return httpHeaders;
            }

            @Override
            public Flux<DataBuffer> getBody() {
              return outputMessage.getBody();
            }
          };
          return chain.filter(exchange.mutate().request(decorator).build());
        }));
  }

  /**
   * 记录到请求日志中去
   *
   * @param exchange request
   * @param body    请求的body内容
   */
  private void recordLog(ServerWebExchange exchange, Object body) {
    ServerHttpRequest request = exchange.getRequest();
    TreeMap<String,Object> params = new TreeMap<>();
    // 记录要访问的url
    StringBuilder builder = new StringBuilder(" request url: ");
    builder.append(request.getURI().getRawPath());

    // 记录访问的方法
    HttpMethod method = request.getMethod();
    if (null != method) {
      builder.append(", method: ").append(method.name());
    }

    // 记录头部信息
    builder.append(", header { ");
    for (Map.Entry<String, List<String>> entry : request.getHeaders().entrySet()) {
      builder.append(entry.getKey()).append(":").append(String.join(",", entry.getValue())).append(",");
    }

    // 记录参数
    builder.append("} param: ");
    // 处理get的请求
    if (null != method && HttpMethod.GET.matches(method.name())) {
      // 记录请求的参数信息 针对GET 请求
      MultiValueMap<String, String> queryParams = request.getQueryParams();
      for (Map.Entry<String, List<String>> entry : queryParams.entrySet()) {
        builder.append(entry.getKey()).append("=").append(String.join(",", entry.getValue())).append(",");
      }
    } else {
      //因为参数后续可能会参与校验   为了后续获取方便,从body中读取参数  将参数保存到exchange的attributes属性中
      builder.append(body);
      JSONObject jsonObject = (JSONObject)JSONObject.toJSON(body);
      params.putAll(jsonObject.getInnerMap());
      exchange.getAttributes().put("CacheRequestFilter",params);
    }
    logger.info(builder.toString());
  }

  @Override
  public int getOrder() {
    //order 配置的是拦截器触发的顺序 ,需要该全局的拦截器在其他拦截器之前,数值越小越靠前
    return 10;
  }
}

用户校验

用户校验只需要对部分接口做校验,因此我们定义了GatewayFilter

spring.cloud.gateway.routes[0].id=web
spring.cloud.gateway.routes[0].uri=lb://api-server
spring.cloud.gateway.routes[0].predicates[0]=Path=/cp/**
spring.cloud.gateway.routes[0].filters[0]=StripPrefix=1
//只有path为cp开头的请求才会走校验
spring.cloud.gateway.routes[0].filters[1]=Sign=true
public class SignGetwayFilter implements GatewayFilter, Ordered {

  private final Logger logger = LoggerFactory.getLogger(SignGetwayFilter.class);

  private final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders();

  private List<String> excludepath;
  private SignConfig signatureConfig;

  public SignGetwayFilter(SignConfig signatureConfig,  List<String> excludepath) {
    this.signatureConfig = signatureConfig;
    this.excludepath = excludepath;
  }

  @Override
  public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    ServerHttpRequest request = exchange.getRequest();
    ServerHttpResponse response = exchange.getResponse();
    HttpMethod method = request.getMethod();
    String path = request.getURI().getPath();
    //excludpath不需要验签
    if (excludepath != null && !excludepath.isEmpty() && excludepath.contains(path)) {
      return chain.filter(exchange);
    }
    if (method == HttpMethod.GET) {
      return ResponseUtils.createErrorResult(response, -1, "不支持get请求");
    }
    String content_type = request.getHeaders().get(HttpHeaders.CONTENT_TYPE).get(0);
    logger.debug("content_type={}", content_type);
    if (StringUtils.isBlank(content_type) || !content_type.equals(MediaType.APPLICATION_JSON_VALUE)) {
      return ResponseUtils.createErrorResult(response, -1, SignStatus.Failed.getMessage());
    }
    DataBufferFactory bufferFactory = response.bufferFactory();
    //获取从Logfilter保存的请求参数
    TreeMap<String,Object> treeMap = exchange.getAttribute("CacheRequestFilter");
    //检验参数
    if(!check(treeMap)){
      //如果未通过返回错误信息
      return ResponseUtils.createErrorResult(response, -1, "未通过");
    }
    return chain.filter(exchange);
  }

  @Override
  public int getOrder() {
    return 100;
  }

}
@Component
public class SignGatewayFilterFactory extends
    AbstractGatewayFilterFactory<SignGatewayFilterFactory.Config> {

  private final Logger logger = LoggerFactory.getLogger(SignGatewayFilterFactory.class);
  @Autowired
  private SignConfig signatureConfig;

  public SignGatewayFilterFactory() {
    super(Config.class);
  }

  @Override
  public SignGetwayFilter apply(Config config) {
    return new SignGetwayFilter(this.signatureConfig, config.getIncludepath(), config.getExcludepath());
  }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 227,283评论 6 530
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 97,947评论 3 413
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 175,094评论 0 373
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 62,485评论 1 308
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 71,268评论 6 405
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 54,817评论 1 321
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 42,906评论 3 440
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 42,039评论 0 285
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 48,551评论 1 331
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 40,502评论 3 354
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 42,662评论 1 366
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 38,188评论 5 356
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 43,907评论 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 34,304评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 35,563评论 1 281
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 51,255评论 3 389
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 47,637评论 2 370

推荐阅读更多精彩内容