OpenFeign不支持{}特殊字符的header解决

一、环境

    <properties>
        <spring.version>5.3.22</spring.version>
        <spring-boot.version>2.7.3</spring-boot.version>
        <spring-cloud.version>3.1.3</spring-cloud.version>
        <spring-cloud-dependencies.version>2021.0.3</spring-cloud-dependencies.version>
        <spring-cloud-starter-alibaba.version>2021.0.1.0</spring-cloud-starter-alibaba.version>
    </properties>

其中feign包的版本号

      <dependency>
        <groupId>io.github.openfeign</groupId>
        <artifactId>feign-bom</artifactId>
        <version>11.8</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>

二、场景描述

feign需要传递一些json格式的数据,代码如下

@Slf4j
public class AuthFeignInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate requestTemplate) {
        UserDetail userDetail = UserContext.getUserDetail();

        if (Objects.nonNull(appDetail)) {
            String userJson = JsonUtils.toJsonString(userDetail);
            requestTemplate.header(Constant.User.HEADER_NAME, userJson);
        }
    }
}

三、 问题定位

userJson携带了特殊符号$:,断点调试的时候参数都是正常设置,尝试定位是生产者还是消费者的问题,使用postman模拟消费者调用,生产者可以正常收到信息并解析成功,那么问题就在消费者

尝试源码断点


public final class RequestTemplate implements Serializable {
    public RequestTemplate header(String name, String... values) {
      return header(name, Arrays.asList(values));
    }

  public RequestTemplate header(String name, Iterable<String> values) {
    if (name == null || name.isEmpty()) {
      throw new IllegalArgumentException("name is required.");
    }
    if (values == null) {
      values = Collections.emptyList();
    }

    return appendHeader(name, values);
  }

  private RequestTemplate appendHeader(String name, Iterable<String> values) {
    if (!values.iterator().hasNext()) {
      /* empty value, clear the existing values */
      this.headers.remove(name);
      return this;
    }
    if (name.equals("Content-Type")) {
      // a client can only produce content of one single type, so always override Content-Type and
      // only add a single type
      this.headers.remove(name);
      this.headers.put(name,
          HeaderTemplate.create(name, Collections.singletonList(values.iterator().next())));
      return this;
    }
    this.headers.compute(name, (headerName, headerTemplate) -> {
      if (headerTemplate == null) {
        return HeaderTemplate.create(headerName, values);
      } else {
        return HeaderTemplate.append(headerTemplate, values);
      }
    });
    return this;
  }
}

可以看到最终是调用HeaderTemplate来实现header设置,继续查看HeaderTemplate源码

public final class HeaderTemplate {

  public static HeaderTemplate create(String name, Iterable<String> values) {
    if (name == null || name.isEmpty()) {
      throw new IllegalArgumentException("name is required.");
    }

    if (values == null) {
      throw new IllegalArgumentException("values are required");
    }

    return new HeaderTemplate(name, values, Util.UTF_8);
  }

  private HeaderTemplate(String name, Iterable<String> values, Charset charset) {
    this.name = name;

    for (String value : values) {
      if (value == null || value.isEmpty()) {
        /* skip */
        continue;
      }

      this.values.add(
          new Template(
              value,
              ExpansionOptions.REQUIRED,
              EncodingOptions.NOT_REQUIRED,
              false,
              charset));
    }
  }
}


可以看到HeaderTemplate只是对Template进行封装


public class Template {

  Template(
      String value, ExpansionOptions allowUnresolved, EncodingOptions encode, boolean encodeSlash,
      Charset charset) {
    if (value == null) {
      throw new IllegalArgumentException("template is required.");
    }
    this.template = value;
    this.allowUnresolved = ExpansionOptions.ALLOW_UNRESOLVED == allowUnresolved;
    this.encode = encode;
    this.encodeSlash = encodeSlash;
    this.charset = charset;
    // 解析${}占位符
    this.parseTemplate();
  }

  private void parseTemplate() {

     // 解析{}占位符
    this.parseFragment(this.template);
  }

  private void parseFragment(String fragment) {
    // 解析每个{}占位符
    ChunkTokenizer tokenizer = new ChunkTokenizer(fragment);

    while (tokenizer.hasNext()) {
      /* check to see if we have an expression or a literal */
      String chunk = tokenizer.next();
      // 如果占位符以{起始,则默认使用模板解析
      if (chunk.startsWith("{")) {
        Expression expression = Expressions.create(chunk);
        if (expression == null) {
          this.templateChunks.add(Literal.create(this.encodeLiteral(chunk)));
        } else {
          this.templateChunks.add(expression);
        }
      } else {
        this.templateChunks.add(Literal.create(this.encodeLiteral(chunk)));
      }
    }
  }
}

到这里基本已经定位到问题是feign实现了高级特性,占位符和模板解析造成的问题,具体还是:的特殊处理,暂时不展开

四、解决

定位到了问题,尝试找解决方案

4.1 占位符设置参数进行替换

查看RestTemplate源码发现有一个resolve方法可以对uriTemplatequeriesheaders进行参数替换

public RequestTemplate resolve(Map<String, ?> variables) {

    StringBuilder uri = new StringBuilder();

    /* create a new template form this one, but explicitly */
    RequestTemplate resolved = RequestTemplate.from(this);

    if (this.uriTemplate == null) {
      /* create a new uri template using the default root */
      this.uriTemplate = UriTemplate.create("", !this.decodeSlash, this.charset);
    }

    String expanded = this.uriTemplate.expand(variables);
    if (expanded != null) {
      uri.append(expanded);
    }

    /*
     * for simplicity, combine the queries into the uri and use the resulting uri to seed the
     * resolved template.
     */
    if (!this.queries.isEmpty()) {
      /*
       * since we only want to keep resolved query values, reset any queries on the resolved copy
       */
      resolved.queries(Collections.emptyMap());
      StringBuilder query = new StringBuilder();
      Iterator<QueryTemplate> queryTemplates = this.queries.values().iterator();

      while (queryTemplates.hasNext()) {
        QueryTemplate queryTemplate = queryTemplates.next();
        String queryExpanded = queryTemplate.expand(variables);
        if (Util.isNotBlank(queryExpanded)) {
          query.append(queryExpanded);
          if (queryTemplates.hasNext()) {
            query.append("&");
          }
        }
      }

      String queryString = query.toString();
      if (!queryString.isEmpty()) {
        Matcher queryMatcher = QUERY_STRING_PATTERN.matcher(uri);
        if (queryMatcher.find()) {
          /* the uri already has a query, so any additional queries should be appended */
          uri.append("&");
        } else {
          uri.append("?");
        }
        uri.append(queryString);
      }
    }

    /* add the uri to result */
    resolved.uri(uri.toString());

    /* headers */
    if (!this.headers.isEmpty()) {
      /*
       * same as the query string, we only want to keep resolved values, so clear the header map on
       * the resolved instance
       */
      resolved.headers(Collections.emptyMap());
      for (HeaderTemplate headerTemplate : this.headers.values()) {
        /* resolve the header */
        String header = headerTemplate.expand(variables);
        if (!header.isEmpty()) {
          /* append the header as a new literal as the value has already been expanded. */
          resolved.header(headerTemplate.getName(), header);
        }
      }
    }

    if (this.bodyTemplate != null) {
      resolved.body(this.bodyTemplate.expand(variables));
    }

    /* mark the new template resolved */
    resolved.resolved = true;
    return resolved;
  }

但是RequestTemplate对象是新生成的,无法进行传递

4.2 替换RequestTemplate的HeaderTemplate

查看源码发现RequestTemplate并没有开发对HeaderTemplate直接注入方法,所有header都是使用header()方法进行处理,而对RequestTemplate的修改都是新生成一个RequestTemplate,放弃这个方案

4.3 参数编码

所有签名都是基于Base64进行字节数组编码,那么该方案的适应性是最好的,对于http协议支持最好,修改源码

@Slf4j
public class AuthFeignInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate requestTemplate) {
        UserDetail userDetail = UserContext.getUserDetail();

        if (Objects.nonNull(appDetail)) {
            String userJson = JsonUtils.toJsonString(userDetail);
            String encodeAppJson = Base64.encode(appJson);
            requestTemplate.header(Constant.User.HEADER_NAME, encodeAppJson);
        }
    }
}

参数解析可以Base64.decode完成解析,还可以兼容中文,不要再进行UTF-8编码,如果考虑历史兼容性,可以先判断header是否以{起始,如果不是则使用Base64解析,如果考虑协议层的可变更性,可以在header中接入类似content-type的编码类型,来实现对变化的支持

五、参考

issue

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值