1

Я пишу приложение на основе микросервиса с сервисами весенне-ботинка.Переписывать внутренние ссылки на eureka на внешние ссылки в прокси zuul

Для связи я использую REST (с ссылками на renoas). Каждая служба регистрируется с помощью eureka, поэтому я предоставляю ссылки, основанные на этих именах, так что ленточные расширенные resttemplates могут использовать функции балансировки нагрузки и восстановления после сбоя.

Это нормально работает для внутренней связи, но у меня есть одностраничное администраторское приложение, которое обращается к сервисам через обратный прокси на основе zuul. Когда ссылки используют реальное имя хоста и порт, ссылки правильно переписаны в соответствии с URL, видимым снаружи. Это, конечно, не работает для символических ссылок, которые мне нужны в внутри ...

Так внутри меня есть ссылки как:

http://adminusers/myfunnyusername 

Zuul прокси должен переписать это

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

Есть ли что-то, что мне не хватает в зууле или где-то по пути, что облегчит это?

Прямо сейчас я думаю, как надежно переписать URL-адреса без побочного ущерба.

Должен быть более простой способ, не так ли?

+0

Там нет такой функциональности в Zuul. Посмотрите на его возможности фильтрации, а точнее на 'org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter' –

+0

Теперь я написал свой собственный ZuulFilter, который анализирует json и фиксирует ссылки. Надеюсь, это не приведет к узкому месту производительности. –

+0

Прохладный - можем ли мы отметить вопрос? –

ответ

4

Причудливо Zuul не может переписывать ссылки с символических имен эврика на «внешние ссылки».

Для этого я просто написал фильтр Zuul, который анализирует ответ json и ищет узлы «ссылок» и переписывает ссылки на мою схему.

Например, мои услуги были названы: adminusers и рестораны Результат от службы имеет ссылки как 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

Томас попросил полный код фильтра, так вот она. Имейте в виду, что он делает некоторые предположения о URL-адресах! Я предполагаю, что внутренние ссылки не содержат порт и имеют имя servicename как хост, что является допустимым предположением для приложений на основе 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; 
    } 
} 

Улучшения радушны :-)

+0

У меня такая же проблема. Вы можете разместить свой последний фильтр здесь? –

2

Посмотрите HATEOAS paths are invalid when using an API Gateway in a Spring Boot app

Если правильно настроены, Zuul следует добавить «X-Forwarded-Host» заголовок для всех пересылаемых запросов, которые Spring-hateoas уважает и изменяют ссылки надлежащим образом.

+0

Спасибо! В некоторых случаях такой подход может работать! Но в моем случае у меня есть ссылки, указывающие на разные сервисы, поэтому с вашим подходом все они будут указывать на одну и ту же услугу, что было бы неправильно. В случае, если у вас есть только «служебные локальные» ссылки, ваш подход будет самым простым и самым дешевым в плане времени и памяти. –

 Смежные вопросы

  • Нет связанных вопросов^_^