将基于 eureka 的内部链接重写为 zuul 代理中的外部链接
Rewrite internal eureka based links to external links in zuul proxy
我正在使用 spring-boot 服务编写基于微服务的应用程序。
为了交流,我使用 REST(带有 hateoas 链接)。每个服务都向eureka注册,所以我提供的链接都是基于这些名字的,这样ribbon enhanced resttemplates就可以使用stack的loadbalancing和failover能力了。
这适用于内部通信,但我有一个单页管理应用程序,它通过基于 zuul 的反向代理访问服务。
当链接使用真实的主机名和端口时,链接会被正确重写以匹配从外部可见的 url。这当然不适用于我在内部需要的符号链接...
所以在内部我有这样的链接:
http://adminusers/myfunnyusername
zuul 代理应该将其重写为
http://localhost:8090/api/adminusers/myfunnyusername
我在 zuul 或沿途的某个地方遗漏了什么可以使这更容易吗?
现在我正在考虑如何可靠地自己重写 urls 而不会造成附带损害。
应该有更简单的方法吧?
看看HATEOAS paths are invalid when using an API Gateway in a Spring Boot app
如果配置正确,ZUUL 应将 "X-Forwarded-Host" header 添加到所有转发的请求中,Spring-hateoas 尊重并适当修改链接。
Aparrently Zuul 无法将 links 从符号尤里卡名称重写为 "outside links"。
为此,我刚刚编写了一个 Zuul 过滤器来解析 json 响应,并查找 "links" 节点并将 link 重写为我的模式。
比如我的服务命名为:adminusers和restaurants
该服务的结果有 link 个,例如 http://adminusers/{id} and http://restaurants/cuisine/{id}
然后改写为
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/");
LOG.info("OLD: {}", href);
LOG.info("NEW: {}", newRef);
href = newRef;
}
return href;
}
(这需要稍微优化一下,因为你只能编译一次正则表达式,一旦我确定这是我真正需要的,我就会这样做 运行)
更新
Thomas 要求提供完整的过滤器代码,所以就在这里。请注意,它对 URL 做了一些假设!我假设内部 links 不包含端口并将服务名称作为主机,这是基于 eureka 的应用程序的有效假设,因为功能区等能够使用这些。我将其重写为 link,例如 $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/");
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;
}
}
欢迎改进:-)
我正在使用 spring-boot 服务编写基于微服务的应用程序。
为了交流,我使用 REST(带有 hateoas 链接)。每个服务都向eureka注册,所以我提供的链接都是基于这些名字的,这样ribbon enhanced resttemplates就可以使用stack的loadbalancing和failover能力了。
这适用于内部通信,但我有一个单页管理应用程序,它通过基于 zuul 的反向代理访问服务。 当链接使用真实的主机名和端口时,链接会被正确重写以匹配从外部可见的 url。这当然不适用于我在内部需要的符号链接...
所以在内部我有这样的链接:
http://adminusers/myfunnyusername
zuul 代理应该将其重写为
http://localhost:8090/api/adminusers/myfunnyusername
我在 zuul 或沿途的某个地方遗漏了什么可以使这更容易吗?
现在我正在考虑如何可靠地自己重写 urls 而不会造成附带损害。
应该有更简单的方法吧?
看看HATEOAS paths are invalid when using an API Gateway in a Spring Boot app
如果配置正确,ZUUL 应将 "X-Forwarded-Host" header 添加到所有转发的请求中,Spring-hateoas 尊重并适当修改链接。
Aparrently Zuul 无法将 links 从符号尤里卡名称重写为 "outside links"。
为此,我刚刚编写了一个 Zuul 过滤器来解析 json 响应,并查找 "links" 节点并将 link 重写为我的模式。
比如我的服务命名为:adminusers和restaurants 该服务的结果有 link 个,例如 http://adminusers/{id} and http://restaurants/cuisine/{id}
然后改写为 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/");
LOG.info("OLD: {}", href);
LOG.info("NEW: {}", newRef);
href = newRef;
}
return href;
}
(这需要稍微优化一下,因为你只能编译一次正则表达式,一旦我确定这是我真正需要的,我就会这样做 运行)
更新
Thomas 要求提供完整的过滤器代码,所以就在这里。请注意,它对 URL 做了一些假设!我假设内部 links 不包含端口并将服务名称作为主机,这是基于 eureka 的应用程序的有效假设,因为功能区等能够使用这些。我将其重写为 link,例如 $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/");
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;
}
}
欢迎改进:-)