我正在用spring-boot服务编写一个基于微服务的应用程序。
对于沟通,我使用REST(与讨厌的链接)。 每个服务都向eureka注册,所以我提供的链接都基于这些名称,以便功能增强的resttemplates可以使用堆栈的负载平衡和故障转移功能。
这适用于内部沟通,但我有一个页面管理员应用程序,通过基于zuul的反向代理访问服务。 当链接使用真实的主机名和端口时,链接被正确地重写以匹配从外部可见的URL。 这当然不适用于我在内部需要的符号链接...
所以内部我有像这样的链接:
http://adminusers/myfunnyusernamezuul代理应该重写这个
http://localhost:8090/api/adminusers/myfunnyusername有没有什么我在zuul或沿途的某个地方可能会让这更容易?
现在我正在考虑如何可靠地重写URL,而不会造成附带损害。
应该有一个更简单的方法,对吧?
I am writing a microservice based application with spring-boot services.
For communication I use REST (with hateoas links). Each service registers with eureka, so I the links I provide are based on these names, so that the ribbon enhanced resttemplates can use the loadbalancing and failover capabilities of the stack.
This works fine for internal communication, but I have a single page admin app that accesses the services through a zuul based reverse proxy. When the links are using the real hostname and port the links are correctly rewritten to match the url visible from the outside. This of course doesn't work for the symbolic links that I need in the inside...
So internally I have links like:
http://adminusers/myfunnyusernameThe zuul proxy should rewrite this to
http://localhost:8090/api/adminusers/myfunnyusernameIs there something that I am missing in zuul or somewhere along the way that would make this easier?
Right now I'm thinking how to reliably rewrite the urls myself without collateral damage.
There should be a simpler way, right?
最满意答案
奇怪的是,Zuul并没有能力重写从象征性的尤里卡名字到“外部链接”的链接。
为此,我编写了一个Zuul过滤器来分析json响应,并查找“链接”节点并将链接重写为我的模式。
例如,我的服务被命名为:adminusers和restaurants服务的结果具有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; }(这需要稍微优化,因为您只能编译一次正则表达式,一旦我确定这是我真正需要的,从长远来看,我会这样做)
UPDATE
托马斯要求提供完整的过滤器代码,所以在这里。 请注意,它对网址做了一些假设! 我假设内部链接不包含端口并且具有作为主机的服务名称,这对于基于尤里卡的应用程序来说是有效的假设,因为功能区等能够与这些应用程序一起工作。 我将其重写为$ 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; } }欢迎改进:-)
Aparrently Zuul is not capable of rewriting links from the symbolic eureka names to "outside links".
For that I just wrote a Zuul filter that parses the json response, and looks for "links" nodes and rewrites the links to my schema.
For example, my services are named: adminusers and restaurants The result from the service has links like http://adminusers/{id} and http://restaurants/cuisine/{id}
Then it would be rewritten to http://localhost:8090/api/adminusers/{id} and 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; }(This needs to be optimized a little, as you could compile the regexp only once, I'll do that once I'm sure that this is what I really need in the long run)
UPDATE
Thomas asked for the full filter code, so here it is. Be aware, it makes some assumptions about the URLs! I assume that internal links do not contain a port and have the servicename as host, which is a valid assumption for eureka based apps, as ribbon etc. are able to work with those. I rewrite that to a link like $PROXY/api/$SERVICENAME/... Feel free to use this code.
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; } }Improvements are welcome :-)
更多推荐
发布评论