Как выборочно инактивировать конечные точки REST в приложении Java?

Я работаю над приложением, которое состоит из нескольких серверных служб и клиентского интерфейса. Все приложение написано на Java, и мы используем веб-сервер Apache TomEE для его запуска.

Внутренние службы предоставляют несколько API и содержат несколько контроллеров. Некоторые из этих API-интерфейсов доступны клиенту веб-интерфейса, а некоторые - для внутренней связи между внутренними службами.

Ведение журнала очень важно для этого приложения. Существует требование, чтобы система ведения журнала всегда инициализировалась перед началом нормальной работы (для обеспечения полной прослеживаемости). Приложение использует защищенную систему ведения журнала, которая требует ключ для инициализации ведения журнала (журналы подписаны с использованием этого ключа, чтобы предотвратить подделку журналов). Существует также требование, чтобы ключ регистрации загружался в каждую службу. Каждый бэкэнд-сервис имеет конечную точку для получения ключа регистрации.

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

Чтобы соответствовать требованиям, мы рассматриваем следующую процедуру запуска:

  1. Запускать бэкэнд-сервисы в сокращенном режиме работы, в котором единственной доступной конечной точкой в ​​каждой службе является та, которая предназначена для получения входящего ключа.
  2. Как только ключ получен и система регистрации инициализирована, активируйте другие конечные точки и начните обычные операции.

Существует ли стандартный способ активации конечных точек для облегчения этого процесса запуска? или в любом случае контроля доступа к конечным точкам.

Некоторая дополнительная информация: классы контроллеров в приложении не расширяют какой-либо другой класс, а украшены только @Path а также @Stateless аннотаций.


Обновление 1

Я следовал подходу использования фильтра (как предложено Богданом ниже). Я создал фильтр, который фиксирует все запросы. Приложение запускается правильно. init() метод в классе фильтра вызывается. Но когда я получаю доступ к /installkey В конечной точке возникает ошибка.

Кажется, что происходит то, что doFilter(ServletRequest, ServletResponse, FilterChain) метод вызывается, и мой код обнаруживает, что запрос для /installkey конечная точка. Но ошибка приходит от вызова: filterChain.doFilter(request, response);,

Я проверил, и я знаю, что переменная filterChain не является nullОднако в рамках метода doFilter(ServletRequest, ServletResponse, FilterChain) что-то идет не так, и я не могу отладить это.

Возможно, я не инициализировал то, что нужно инициализировать.

Я добавил вывод, который я получаю ниже.

Теперь у меня есть следующее в моем web.xml:

<filter>
    <filter-name>myFilter</filter-name>
    <filter-class>com.company.filter.LoggingFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>myFilter</filter-name>
    <url-pattern>*</url-pattern>
</filter-mapping>

И следующий класс:

public class LoggingFilter implements Filter {

    @Override
    public void init(final FilterConfig filterConfig) throws ServletException {

    }

    public void doFilter(final ServletRequest request, 
                         final ServletResponse response, 
                         final FilterChain filterChain) throws IOException, 
                                                               ServletException {

        String url = "";
        if (request instanceof HttpServletRequest) {
            url = ((HttpServletRequest) request).getRequestURL().toString();
        }

        if (url.endsWith("/installkey/")) {
            filterChain.doFilter(request, response);
            return;
        } else if (loggerConfig.isInitialized()) {
            filterChain.doFilter(request, response);
            return;
        }
    }

    public void destroy() {
        System.out.println("XXXXXXXXXXX Running destroy");
    }
}

Но я получаю следующую ошибку:

Jan 19, 2016 10:42:25 AM org.apache.catalina.core.StandardWrapperValve invoke
SEVERE: Servlet.service() for servlet [default] in context with path [/vw-ws-rest] threw exception [Error processing webservice request] with root cause
java.lang.NullPointerException
    at org.apache.cxf.transport.http.AbstractHTTPDestination.invoke(AbstractHTTPDestination.java:240)
    at org.apache.openejb.server.cxf.rs.CxfRsHttpListener.doInvoke(CxfRsHttpListener.java:227)
    at org.apache.tomee.webservices.CXFJAXRSFilter.doFilter(CXFJAXRSFilter.java:94)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
    at com.company.filter.LoggingFilter.doFilter(LoggingFilter.java:63)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:220)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:122)
    at org.apache.tomee.catalina.OpenEJBValve.invoke(OpenEJBValve.java:44)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:505)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:170)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:103)
    at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:957)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:116)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:423)
    at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1079)
    at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:620)
    at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:316)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:745)

Обновление 2

В качестве альтернативы я попробовал подход с использованием привязки имен JAX-RS, как предложил Кассио Маззочи Молин.

Я создал интерфейс:

import javax.ws.rs.NameBinding;

@NameBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD })
public @interface TemporarilyDisabled {
}

И я создал класс фильтра следующим образом:

@Provider
@TemporarilyDisabled
public class LoggingFilter implements ContainerRequestFilter {

    @Override
    public void filter(final ContainerRequestContext requestContext) throws IOException {

        System.out.println("in filter method!");
    }
}

И обновил мой класс контроллера ресурсов следующим образом:

@Path("installkey")
@Stateless(name = "vw-installKeyResource")
public class VwInstallKeyResource {

    @Inject
    private Logger LOG;

    @EJB
    //... some required classes

    @POST
    @TemporarilyDisabled
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response savePlatformData(final InstallKeyData installKeyData)
        throws CryptographicOperationException, DuplicateEntryException {

        ....
    }
}

Это приложение использует Java EE 6, который я не могу обновить. Чтобы проверить этот подход, мне пришлось добавить следующую зависимость в приложение:

<!-- JAX-RS -->
<dependency>
    <groupId>javax.ws.rs</groupId>
    <artifactId>javax.ws.rs-api</artifactId>
    <version>2.0</version>
</dependency>

Код все компилируется нормально, и приложение запускается нормально.

Но когда я получаю доступ к конечной точке (конечной точке, которая должна быть перехвачена фильтром), тогда код фильтра не выполняется (я никогда не вижу оператор печати в методе фильтра), и конечная точка просто выполняется как обычно.

По какой-то причине фильтр не захватывает запрос.

Я не знаю, связана ли проблема с тем, что конечной точкой является POST. В качестве альтернативы, возможно, JAX-RS не находит класс фильтра, он украшен @provider, но я не знаю, нужно ли мне регистрировать фильтр каким-либо другим способом.

2 ответа

Я не думаю, что вы найдете какое-либо нестандартное решение для этого.

При использовании JAX-RS 2.0 и его реализаций вы найдете несколько полезных ресурсов: вы можете использовать фильтр привязки имен, чтобы прервать запрос к определенной конечной точке, в зависимости от ваших условий.

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

Определение аннотации привязки имени

Чтобы привязать фильтры к вашим конечным точкам REST, JAX-RS 2.0 предоставляет метааннотацию @NameBinding, Его можно использовать для создания других аннотаций, которые будут использоваться для привязки фильтров к вашим конечным точкам JAX-RS.

Рассмотрим @TemporarilyDisabled аннотация определена ниже. Это комментируется @NameBinding:

@NameBinding
@Retention(RUNTIME)
@Target({TYPE, METHOD})
public @interface TemporarilyDisabled { }

Предотвращение HTTP-запроса для достижения ваших конечных точек

@TemporarilyDisabled аннотация, созданная выше, будет использоваться для украшения класса фильтра, который реализует ContainerRequestFilter, что позволяет отменить запрос:

@Provider
@TemporarilyDisabled
public class TemporarilyDisableEndpointRequestFilter implements ContainerRequestFilter {

    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {

        if (isEndpointTemporarilyDisabled) {
            requestContext.abortWith(Response.status(Response.Status.SERVICE_UNAVAILABLE)
                                        .entity("Service temporarily unavailable.")
                                        .build());
        }
    }
}

@Provider аннотация отмечает реализацию интерфейса расширения, который должен быть обнаружен во время выполнения JAX-RS на этапе сканирования провайдера.

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

В приведенном выше примере:

  • Если isEndpointTemporarilyDisabled состояние оценивается в true, запрос будет прерван с HTTP 503 Service Unavailable ответ.

  • Если isEndpointTemporarilyDisabled оценивается в false запрос не будет прерван и достигнет конечной точки, запрошенной пользователем.

Зачем прерывать запрос с 503 Service Unavailable ответ?

Согласно RFC 2616, HTTP 503 Service Unavailable следует использовать в следующих ситуациях:

10.5.4 Сервис 503 недоступен

Сервер в настоящее время не может обработать запрос из-за временной перегрузки или обслуживания сервера. Подразумевается, что это временное состояние, которое будет смягчено после некоторой задержки. Если известно, длительность задержки МОЖЕТ быть указана в виде Retry-After заголовок. Если нет Retry-After дается, клиент ДОЛЖЕН обработать ответ, как это было бы для 500 ответ.

Примечание: существование 503 Код состояния не означает, что сервер должен использовать его при перегрузке. Некоторые серверы могут просто отказаться от подключения.

Привязка фильтра к вашим конечным точкам

Чтобы связать фильтр с вашими методами или классами конечных точек, аннотируйте их @TemporarilyDisabled аннотация, определенная выше. Для аннотированных методов и / или классов фильтр будет выполнен:

@Path("/")
public class MyEndpoint {

    @GET
    @Path("{id}")
    @Produces("application/json")
    public Response myMethod(@PathParam("id") Long id) {
        // This method is not annotated with @TemporarilyDisabled
        // The request filter won't be executed when invoking this method
        ...
    }

    @DELETE
    @Path("{id}")
    @TemporarilyDisabled
    @Produces("application/json")
    public Response myTemporarilyDisabledMethod(@PathParam("id") Long id) {
        // This method is annotated with @TemporarilyDisabled
        // The request filter will be executed when invoking this method
        ...
    }
}

В приведенном выше примере фильтр запроса будет выполняться только для myTemporarilyDisabledMethod(Long) метод, потому что он аннотирован @TemporarilyDisabled,

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

Если флаг установлен (то есть входящий ключ был успешно установлен), вы разрешаете доступ к конечным точкам, в противном случае из фильтра вы возвращаете какой-либо статус (401 или 403). Если вы установите этот флаг в памяти или где-нибудь быстро, чтобы прочитать служебные данные фильтра должны быть достаточно малы, чтобы их можно было игнорировать.

Другие вопросы по тегам