Статьи

Перенаправление HTTP-запросов с Zuul в Spring Boot

Zuul является частью пакета  Spring Cloud Netflix и позволяет перенаправлять запросы REST для выполнения фильтров различных типов.

Практически в любом проекте, где мы используем микросервисы, желательно, чтобы все коммуникации между этими микросервисами проходили через общее место, чтобы входы и выходы регистрировались и могли реализовывать запросы безопасности или перенаправления в зависимости от различных параметров.

С Zuul , это очень легко осуществить , так как он прекрасно интегрирован с Spring ботинком.

Как всегда, вы можете увидеть источники, на которых основана эта статья,  на моей странице GitHub . Итак, давайте вернемся к этому.

Создание проекта

Если вы установили Eclipse с  плагином для Spring Boot (что я рекомендую), создать проект так же просто, как добавить новый  тип проекта Spring Boot , включая Zuul  Starter . Для выполнения некоторых тестов мы также включим веб-  стартер , как показано на рисунке ниже:


У нас также есть возможность создать проект Maven из  https://start.spring.io/ . Затем мы импортируем необходимую информацию из нашей предпочитаемой среды IDE.


начало

Предположим, что программа прослушивает http: // localhost: 8080 / , и мы хотим, чтобы все запросы на URL  http: // localhost: 8080 / google были перенаправлены на https://www.google.com. ,

Для этого мы создаем  application.yml файл в  resources каталоге, как показано на рисунке ниже:

Этот файл будет содержать следующие строки:

zuul:  
  routes:
    google:
      path: /google/**
      url: https://www.google.com/

Они указывают, что все запрошенное с помощью пути / google / и других (**) будет перенаправлено на https://www.google.com/ . Если такой запрос сделан, http://localhost:8080/google/search?q=profesor_pон будет перенаправлен на https://www.google.com/search?q=profesor_p. Другими словами, то, что мы добавим после / google /, будет включено в перенаправление из-за двух звездочек, добавленных в конце пути.

Чтобы программа работала, нужно будет только добавить аннотацию @EnableZuulProxy и начальный класс, в данном случае: ZuulSpringTestApplication

import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@SpringBootApplication
@EnableZuulProxy
public class ZuulSpringTestApplication {
  public static void main(String[] args) {
  SpringApplication.run(ZuulSpringTestApplication.class, args);
  }
}

Чтобы продемонстрировать различные возможности Zuul,  http: // localhost: 8080 / api будет прослушивать REST-сервис, реализованный в TestController классе этого проекта. Этот класс просто возвращает в теле данные полученного запроса.

@RestController
public class TestController {
 final static String SALTOLINEA = "\n";

 Logger log = LoggerFactory.getLogger(TestController.class);
 @RequestMapping(path = "/api")
 public String test(HttpServletRequest request) {
  StringBuffer strLog = new StringBuffer();

  strLog.append("................ RECIBIDA PETICION EN /api ......  " + SALTOLINEA);
  strLog.append("Metodo: " + request.getMethod() + SALTOLINEA);
  strLog.append("URL: " + request.getRequestURL() + SALTOLINEA);
  strLog.append("Host Remoto: " + request.getRemoteHost() + SALTOLINEA);
  strLog.append("----- MAP ----" + SALTOLINEA);
  request.getParameterMap().forEach((key, value) -> {
   for (int n = 0; n < value.length; n++) {
    strLog.append("Clave:" + key + " Valor: " + value[n] + SALTOLINEA);
   }
  });

  strLog.append(SALTOLINEA + "----- Headers ----" + SALTOLINEA);
  Enumeration < String > nameHeaders = request.getHeaderNames();
  while (nameHeaders.hasMoreElements()) {
   String name = nameHeaders.nextElement();
   Enumeration < String > valueHeaders = request.getHeaders(name);
   while (valueHeaders.hasMoreElements()) {
    String value = valueHeaders.nextElement();
    strLog.append("Clave:" + name + " Valor: " + value + SALTOLINEA);
   }
  }
  try {
   strLog.append(SALTOLINEA + "----- BODY ----" + SALTOLINEA);
   BufferedReader reader = request.getReader();
   if (reader != null) {
    char[] linea = new char[100];
    int nCaracteres;
    while ((nCaracteres = reader.read(linea, 0, 100)) > 0) {
     strLog.append(linea);

     if (nCaracteres != 100)
      break;
    }
   }
  } catch (Throwable e) {
   e.printStackTrace();
  }
  log.info(strLog.toString());

  return SALTOLINEA + "---------- Prueba de ZUUL ------------" + SALTOLINEA +
   strLog.toString();
 }
}

Фильтрация: запись логов

В этой части мы увидим, как создать фильтр, чтобы оставить запись сделанных запросов.

Для этого мы создадим класс, PreFilter.javaкоторый должен расширяться  ZuulFilter:

public class PreFilter extends ZuulFilter {
 Logger log = LoggerFactory.getLogger(PreFilter.class);
 @Override
 public Object run() {
  RequestContext ctx = RequestContext.getCurrentContext();
  StringBuffer strLog = new StringBuffer();
  strLog.append("\n------ NUEVA PETICION ------\n");
  strLog.append(String.format("Server: %s Metodo: %s Path: %s \n", ctx.getRequest().getServerName(), ctx.getRequest().getMethod(),
   ctx.getRequest().getRequestURI()));
  Enumeration < String > enume = ctx.getRequest().getHeaderNames();
  String header;
  while (enume.hasMoreElements()) {
   header = enume.nextElement();
   strLog.append(String.format("Headers: %s = %s \n", header, ctx.getRequest().getHeader(header)));
  };
  log.info(strLog.toString());
  return null;
 }

 @Override
 public boolean shouldFilter() {
  return true;
 }

 @Override
 public int filterOrder() {
  return FilterConstants.SEND_RESPONSE_FILTER_ORDER;
 }

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

}

В этом классе мы перезапишем функции, которые мы видим в источнике. Давайте объясним каждую из этих функций:

  • public Object run ()  — запускается для каждого полученного запроса. Здесь мы можем увидеть содержимое запроса и обработать его при необходимости.
  • общественного логического shouldFilter ()  — Если она возвращает истиннуюrun  функция будет выполнена.
  • public int filterOrder () — Возвращает, когда этот фильтр выполняется, потому что обычно есть разные фильтры для каждой задачи. Мы должны принять во внимание, что некоторые перенаправления или изменения в петиции должны выполняться в определенном порядке, по той же логике, что и Zuul при обработке запросов.
  • public String Filtertype ()  — указывает, когда выполняется фильтр. Если он возвращает « pre », он выполняется до того, как они произвели перенаправление, и, следовательно, до того, как он был вызван конечным сервером (в нашем примере для Google). Если он возвращает «сообщение», выполняется после того, как сервер ответил. В  org.springframework.cloud.netflix.zuul.filters.support.FilterConstantsклассе мы определили типы, которые должны быть возвращены: PRE_TYPE, POST_TYPE, ERROR_TYPE или ROUTE_TYPE.

В примере класса мы видим, что перед выполнением запроса к серверу некоторые данные запроса записываются, оставляя журнал.

Наконец, чтобы Spring Boot использовал этот фильтр, мы должны добавить следующую функцию в наш класс.

@Bean
public PreFilter preFilter() {
        return new PreFilter();
 }

Zuul  выглядит для бобов , чтобы наследовать от класса,  ZuulFilter,  и использовать их.

В этом примере PostFilter класс Java   также реализует другой фильтр, но запускается только после выполнения запроса к серверу. Как я уже говорил, это достигается путем возврата « поста » в  Filtertype() функцию.

Чтобы Zuul использовал этот класс, мы создадим еще один компонент с такой функцией:

 @Bean
 public PostFilter postFilter() {
        return new PostFilter();
 }

Помните, что есть также фильтр для обработки ошибок, которые необходимо устранить сразу после перенаправления («маршрут»), но в этой статье рассматриваются только  типы записей  и предварительные фильтры.

Я хотел бы уточнить, что, хотя эта статья не относится к ней,   Zuul  может перенаправлять не только на статический URL-адрес, но и на службы, предоставляемые сервером Eureka. Он также интегрируется с Hystrix для обеспечения отказоустойчивости, поэтому, если сервер не может связаться, вы можете указать, какое действие предпринять.

Фильтрация и реализация безопасности

Давайте добавим новое перенаправление файлов в файл application.yml.

Это перенаправление перенаправит любой тип запроса с  http: // localhost: 8080 / private / foo на страницу, где размещена эта статья ( http://www.profesor-p.com ).

Линия sensitiveHeaders будет объяснена позже.

В  PreRewriteFilterклассе я реализовал еще один предварительный фильтр для устранения этого перенаправления. Как? Легко. Поместите этот код в  shouldFilter() функцию.

@Override
public boolean shouldFilter() {
  return RequestContext.getCurrentContext().getRequest()
         .getRequestURI().startsWith("/privado");
}

Теперь в  run функцию мы включили следующий код:

@Override
public Object run() {
 RequestContext ctx = RequestContext.getCurrentContext();
 StringBuffer strLog = new StringBuffer();
 strLog.append("\n------ FILTRANDO ACCESO A PRIVADO - PREREWRITE FILTER  ------\n");

 try {
  String url = UriComponentsBuilder.fromHttpUrl("http://localhost:8080/").path("/api").build().toUriString();
  String usuario = ctx.getRequest().getHeader("usuario") == null ? "" : ctx.getRequest().getHeader("usuario");
  String password = ctx.getRequest().getHeader("clave") == null ? "" : ctx.getRequest().getHeader("clave");

  if (!usuario.equals("")) {
   if (!usuario.equals("profesorp") || !password.equals("profe")) {
    String msgError = "Usuario y/o contraseña invalidos";
    strLog.append("\n" + msgError + "\n");
    ctx.setResponseBody(msgError);
    ctx.setResponseStatusCode(HttpStatus.FORBIDDEN.value());
    ctx.setSendZuulResponse(false);
    log.info(strLog.toString());
    return null;
   }
   ctx.setRouteHost(new URL(url));
  }
 } catch (IOException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
 }

 log.info(strLog.toString());
 return null;
}

Это ищет заголовки запроса ( заголовки ) и, если пользовательский заголовок не существует, он ничего не делает, и запрос перенаправляется http://www.profesor-p.com. Если  найден заголовок пользователя, который имеет значение  profesorp, а ключ переменной  имеет значение profe, запрос перенаправляется http://localhost:8080/api. В противном случае он возвращает HTTP-код,  запрещенный , возвращая строку "Invalid username and/or password"в теле HTTP-ответа. Более того, поток запроса отменяется, потому что он вызывает ctx.setSendZuulResponse (false) .

Поскольку строка  sensitiveHeaders в файле application.yml, о которой я упоминал выше, имеет заголовки user ‘ и ‘ password ‘, она не будет передана в поток запроса.

Очень важно, чтобы этот фильтр запускался после фильтра PRE_DECORATION, потому что в противном случае вызов не ctx.setRouteHost()будет иметь никакого эффекта. Следовательно,   filterOrder функция будет иметь такой код:

@Override
public int filterOrder() {
   return FilterConstants.PRE_DECORATION_FILTER_ORDER+1; 
}

Таким образом, при вызове, передающем пользователя и правильный пароль, мы перенаправим на http: // localhost: 8080 / api.

> curl -s -H "usuario: profesorp" -H "clave: profe" localhost:8080/privado

---------- Prueba de ZUUL ------------
................ RECIBIDA PETICION EN /api ......
Metodo: GET
URL: http://localhost:8080/api
Host Remoto: 127.0.0.1
----- MAP ----

----- Headers ----
Clave:user-agent Valor: curl/7.63.0
Clave:accept Valor: */*
Clave:x-forwarded-host Valor: localhost:8080
Clave:x-forwarded-proto Valor: http
Clave:x-forwarded-prefix Valor: /privado
Clave:x-forwarded-port Valor: 8080
Clave:x-forwarded-for Valor: 0:0:0:0:0:0:0:1
Clave:accept-encoding Valor: gzip
Clave:host Valor: localhost:8080
Clave:connection Valor: Keep-Alive

----- BODY ----

Если вы введете неправильный пароль, результат будет выглядеть так:

 > curl -s -H "usuario: profesorp" -H "clave: ERROR" localhost:8080/privado
Usuario y/o contraseña invalidos

Фильтрация: динамический фильтр

Наконец, мы включим два новых перенаправления в файл applicaction.yml

local:
    path: /local/**
    url: http://localhost:8080/api
 url:
    path: /url/**
    url: http://url.com

В первых трех строках, когда мы перейдем по URL, http://localhost:8080/local/XXXXмы будем перенаправлены на http://localhost:8080/api/XXX. Я поясню, что метка localпроизвольная, и мы могли бы поставить json: так, чтобы она не совпадала с той path , на  которую  мы хотим перенаправить.

Во вторых трех строках, когда мы перейдем к URL, http://localhost:8080/url/XXXXмы будем перенаправлены наhttp://localhost:8080/api/XXXXX

RouteURLFilter Класс  будет нести ответственность за выполнение данных в URL — фильтра. Помните, что для использования  Zuul фильтры должны создать соответствующий  компонент.

@Bean
 public RouteURLFilter routerFilter() {
        return new RouteURLFilter();
 }

В  shouldFilter функции  RouteURLFilter,  мы имеем этот код только выполнять запросы  / URL.

@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
if ( ctx.getRequest().getRequestURI() == null || 
        ! ctx.getRequest().getRequestURI().startsWith("/url"))
return false;
return ctx.getRouteHost() != null && ctx.sendZuulResponse();
}

В  run функции у нас есть код, который выполняет магию. Как только мы уловим целевой URL и путь, как я объясню ниже, он используется в  setRouteHost() функции  RequestContext для правильного перенаправления наших запросов.

@Override
public Object run() {
 try {
  RequestContext ctx = RequestContext.getCurrentContext();
  URIRequest uriRequest;
  try {
   uriRequest = getURIRedirection(ctx);
  } catch (ParseException k) {
   ctx.setResponseBody(k.getMessage());
   ctx.setResponseStatusCode(HttpStatus.BAD_REQUEST.value());
   ctx.setSendZuulResponse(false);
   return null;
  }

  UriComponentsBuilder uriComponent = UriComponentsBuilder.fromHttpUrl(uriRequest.getUrl());
  if (uriRequest.getPath() == null)
   uriRequest.setPath("/");
  uriComponent.path(uriRequest.getPath());

  String uri = uriComponent.build().toUriString();
  ctx.setRouteHost(new URL(uri));
 } catch (IOException k) {
  k.printStackTrace();
 }
 return null;
}

Он ищет переменные   hostDestino и   pathDestino в заголовке, чтобы создать новый URL-адрес, на который он должен перенаправить.

Например, предположим, что у нас есть такой запрос:

> curl --header "hostDestino: http://localhost:8080" --header "pathDestino: api" \
localhost:8080/url?nombre=profesorp

Вызов будет перенаправлен на  http: // localhost: 8080 / api? Q = profesor-p  и выведет следующий вывод:

--------- Prueba de ZUUL ------------
................ RECIBIDA PETICION EN /api ......
Metodo: GET
URL: http://localhost:8080/api
Host Remoto: 127.0.0.1
----- MAP ----
Clave:nombre Valor: profesorp

----- Headers ----
Clave:user-agent Valor: curl/7.60.0
Clave:accept Valor: */*
Clave:hostdestino Valor: http://localhost:8080
Clave:pathdestino Valor: api
Clave:x-forwarded-host Valor: localhost:8080
Clave:x-forwarded-proto Valor: http
Clave:x-forwarded-prefix Valor: /url
Clave:x-forwarded-port Valor: 8080
Clave:x-forwarded-for Valor: 0:0:0:0:0:0:0:1
Clave:accept-encoding Valor: gzip
Clave:host Valor: localhost:8080
Clave:connection Valor: Keep-Alive

----- BODY ----

Вы также можете получить URL для перенаправления тела запроса. Полученный объект JSON должен иметь формат, определенный   GatewayRequest классом, который, в свою очередь, содержит  URIRequest объект.

public class GatewayRequest {
  URIRequest uri;
  String body;
}

public class URIRequest {
  String url;
  String path;
  byte[] body=null;
}

Это пример помещения адреса перенаправления URL в тело:

> curl -X POST \
  'http://localhost:8080/url?nombre=profesorp' \
  -H 'Content-Type: application/json' \
  -d '{
    "body": "The body", "uri": { "url":"http://localhost:8080", "path": "api"    }
}'

  ---------- Prueba de ZUUL ------------
................ RECIBIDA PETICION EN /api ......
Metodo: POST
URL: http://localhost:8080/api
Host Remoto: 127.0.0.1
----- MAP ----
Clave:nombre Valor: profesorp

----- Headers ----
Clave:user-agent Valor: curl/7.60.0
Clave:accept Valor: */*
Clave:content-type Valor: application/json
Clave:x-forwarded-host Valor: localhost:8080
Clave:x-forwarded-proto Valor: http
Clave:x-forwarded-prefix Valor: /url
Clave:x-forwarded-port Valor: 8080
Clave:x-forwarded-for Valor: 0:0:0:0:0:0:0:1
Clave:accept-encoding Valor: gzip
Clave:content-length Valor: 91
Clave:host Valor: localhost:8080
Clave:connection Valor: Keep-Alive

----- BODY ----
The body 

Поскольку тело обрабатывается, мы отправляем на сервер только то, что отправлено в  bodyпараметре запроса JSON.

Как показано, Zuul обладает большой мощностью и является отличным инструментом для перенаправлений. В этой статье я только коснулся основных функций этого фантастического инструмента, но я надеюсь, что он позволил вам увидеть возможности.

Эта статья была написана первоначально на испанском языке, и вы можете найти ее здесь .

Вы можете найти меня в Twitter здесь .