【问题标题】:Rewrite internal eureka based links to external links in zuul proxy将基于 eureka 的内部链接重写为 zuul 代理中的外部链接
【发布时间】:2015-08-14 00:31:29
【问题描述】:

我正在使用 spring-boot 服务编写一个基于微服务的应用程序。

我使用 REST(带有 hatoas 链接)进行通信。每个服务都向 eureka 注册,所以我提供的链接都是基于这些名称的,因此功能区增强的 resttemplates 可以使用堆栈的负载平衡和故障转移功能。

这适用于内部通信,但我有一个单页管理应用程序,它通过基于 zuul 的反向代理访问服务。 当链接使用真实的主机名和端口时,链接被正确重写以匹配从外部可见的 url。这当然不适用于我在内部需要的符号链接......

所以在内部我有如下链接:

http://adminusers/myfunnyusername

zuul 代理应该将其重写为

http://localhost:8090/api/adminusers/myfunnyusername

在 zuul 或其他地方有什么我遗漏的东西可以让这更容易吗?

现在我正在考虑如何可靠地自己重写网址而不会造成附带损害。

应该有更简单的方法吧?

【问题讨论】:

  • ZUUL 中没有这样的功能。看看它的过滤能力,更具体地说是org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter
  • 我现在已经编写了自己的 ZuulFilter 来解析 json 并修复链接。我希望这不会导致以后出现性能瓶颈。
  • 酷 - 我们可以标记问题已回答吗?

标签: spring-boot hateoas spring-cloud netflix-eureka netflix-zuul


【解决方案1】:

看看HATEOAS paths are invalid when using an API Gateway in a Spring Boot app

如果配置正确,ZUUL 应该将“X-Forwarded-Host”标头添加到所有转发的请求中,Spring-hateoas 尊重并适当地修改链接。

【讨论】:

  • 谢谢!这种方法在某些情况下可能会奏效!但是在我的情况下,我也有指向不同服务的链接,因此使用您的方法,所有这些链接都会指向相同的服务,这是错误的。如果您只有“本地服务”链接,您的方法将是最简单、最便宜的时间和内存条件。
【解决方案2】:

显然 Zuul 无法将链接从符号尤里卡名称重写为“外部链接”。

为此,我刚刚编写了一个解析 json 响应的 Zuul 过滤器,并查找“链接”节点并将链接重写为我的架构。

例如,我的服务被命名为:adminusers 和 restaurant 服务的结果包含 http://adminusers/{id} 和 http://restaurants/cuisine/{id}

之类的链接

然后它将被重写为 http://localhost:8090/api/adminusers/{id} 和 http://localhost:8090/api/restaurants/cuisine/{id}

private String fixLink(String href) {
    //Right now all "real" links contain ports and loadbalanced links not
    //TODO: precompile regexes
    if (!href.matches("http[s]{0,1}://[a-zA-Z0-9]+:[0-9]+.*")) {
        String newRef = href.replaceAll("http[s]{0,1}://([a-zA-Z0-9]+)", BasicLinkBuilder.linkToCurrentMapping().toString() + "/api/$1");
        LOG.info("OLD: {}", href);
        LOG.info("NEW: {}", newRef);
        href = newRef;
    }
    return href;
}

(这需要稍微优化一下,因为你只能编译一次正则表达式,一旦我确定这是我长期真正需要的,我就会这样做)

更新

Thomas 要求提供完整的过滤器代码,所以在这里。请注意,它对 URL 做了一些假设!我假设内部链接不包含端口并且将服务名称作为主机,这对于基于 eureka 的应用程序是一个有效的假设,因为功能区等能够使用这些。我将其重写为 $PROXY/api/$SERVICENAME/... 之类的链接 随意使用此代码。

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.CharStreams;
import com.netflix.util.Pair;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.hateoas.mvc.BasicLinkBuilder;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.regex.Pattern;

import static com.google.common.base.Preconditions.checkNotNull;

@Component
public final class ContentUrlRewritingFilter extends ZuulFilter {
    private static final Logger LOG = LoggerFactory.getLogger(ContentUrlRewritingFilter.class);

    private static final String CONTENT_TYPE = "Content-Type";

    private static final ImmutableSet<MediaType> DEFAULT_SUPPORTED_TYPES = ImmutableSet.of(MediaType.APPLICATION_JSON);

    private final String replacement;
    private final ImmutableSet<MediaType> supportedTypes;
    //Right now all "real" links contain ports and loadbalanced links not
    private final Pattern detectPattern = Pattern.compile("http[s]{0,1}://[a-zA-Z0-9]+:[0-9]+.*");
    private final Pattern replacePattern;

    public ContentUrlRewritingFilter() {
        this.replacement = checkNotNull("/api/$1");
        this.supportedTypes = ImmutableSet.copyOf(checkNotNull(DEFAULT_SUPPORTED_TYPES));
        replacePattern = Pattern.compile("http[s]{0,1}://([a-zA-Z0-9]+)");
    }

    private static boolean containsContent(final RequestContext context) {
        assert context != null;
        return context.getResponseDataStream() != null || context.getResponseBody() != null;
    }

    private static boolean supportsType(final RequestContext context, final Collection<MediaType> supportedTypes) {
        assert supportedTypes != null;
        for (MediaType supportedType : supportedTypes) {
            if (supportedType.isCompatibleWith(getResponseMediaType(context))) return true;
        }
        return false;
    }

    private static MediaType getResponseMediaType(final RequestContext context) {
        assert context != null;
        for (final Pair<String, String> header : context.getZuulResponseHeaders()) {
            if (header.first().equalsIgnoreCase(CONTENT_TYPE)) {
                return MediaType.parseMediaType(header.second());
            }
        }
        return MediaType.APPLICATION_OCTET_STREAM;
    }

    @Override
    public String filterType() {
        return "post";
    }

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

    @Override
    public boolean shouldFilter() {
        final RequestContext context = RequestContext.getCurrentContext();
        return hasSupportedBody(context);
    }

    public boolean hasSupportedBody(RequestContext context) {
        return containsContent(context) && supportsType(context, this.supportedTypes);
    }

    @Override
    public Object run() {
        try {
            rewriteContent(RequestContext.getCurrentContext());
        } catch (final Exception e) {
            Throwables.propagate(e);
        }
        return null;
    }

    private void rewriteContent(final RequestContext context) throws Exception {
        assert context != null;
        String responseBody = getResponseBody(context);
        if (responseBody != null) {
            ObjectMapper mapper = new ObjectMapper();
            LinkedHashMap<String, Object> map = mapper.readValue(responseBody, LinkedHashMap.class);
            traverse(map);
            String body = mapper.writeValueAsString(map);
            context.setResponseBody(body);
        }
    }

    private String getResponseBody(RequestContext context) throws IOException {
        String responseData = null;
        if (context.getResponseBody() != null) {
            context.getResponse().setCharacterEncoding("UTF-8");
            responseData = context.getResponseBody();

        } else if (context.getResponseDataStream() != null) {
            context.getResponse().setCharacterEncoding("UTF-8");
            try (final InputStream responseDataStream = context.getResponseDataStream()) {
                //FIXME What about character encoding of the stream (depends on the response content type)?
                responseData = CharStreams.toString(new InputStreamReader(responseDataStream));
            }
        }
        return responseData;
    }

    private void traverse(Map<String, Object> node) {
        for (Map.Entry<String, Object> entry : node.entrySet()) {
            if (entry.getKey().equalsIgnoreCase("links") && entry.getValue() instanceof Collection) {
                replaceLinks((Collection<Map<String, String>>) entry.getValue());
            } else {
                if (entry.getValue() instanceof Collection) {
                    traverse((Collection) entry.getValue());
                } else if (entry.getValue() instanceof Map) {
                    traverse((Map<String, Object>) entry.getValue());
                }
            }
        }
    }

    private void traverse(Collection<Map> value) {
        for (Object entry : value) {
            if (entry instanceof Collection) {
                traverse((Collection) entry);
            } else if (entry instanceof Map) {
                traverse((Map<String, Object>) entry);
            }
        }
    }

    private void replaceLinks(Collection<Map<String, String>> value) {
        for (Map<String, String> node : value) {
            if (node.containsKey("href")) {
                node.put("href", fixLink(node.get("href")));
            } else {
                LOG.debug("Link Node did not contain href! {}", value.toString());
            }
        }
    }

    private String fixLink(String href) {
        if (!detectPattern.matcher(href).matches()) {
            href = replacePattern.matcher(href).replaceAll(BasicLinkBuilder.linkToCurrentMapping().toString() + replacement);
        }
        return href;
    }
}

欢迎改进:-)

【讨论】:

  • 我也有同样的问题。您可以在这里发布您的最终过滤器吗?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-07-28
  • 2012-02-08
相关资源
最近更新 更多