如何在Java应用程序中有选择地停用REST端点?

时间:2016-01-17 15:37:12

标签: rest java-ee endpoint tomee

我正在开发一个由多个后端服务和前端客户端组成的应用程序。整个应用程序是用Java编写的,我们使用Apache TomEE Web服务器来运行它。

后端服务公开了几个API,并包含多个控制器。其中一些API可供前端客户端访问,一些API用于后端服务之间的内部通信。

记录对于此应用程序非常重要。要求在开始正常操作之前始终初始化日志记录系统(以确保完全可追溯性)。该应用程序使用安全日志记录系统,该系统需要密钥来初始化日志记录(使用此密钥对日志进行签名以防止篡改日志)。还需要将日志记录密钥上载到每个服务。每个后端服务都有一个端点,用于接收日志密钥。

存在“鸡或蛋”类型的问题。应用程序需要运行才能接收密钥,但在收到密钥之前,应用程序也不应完全正常运行。

为了满足要求,我们正在考虑以下启动程序:

  1. 以简化操作模式启动后端服务,其中每个服务中唯一可访问的端点是用于接收传入密钥的端点。
  2. 收到密钥后,日志系统初始化,然后激活其他端点,并开始正常操作。
  3. 是否有一种激活端点的标准方法来促进此启动过程?或者无论如何控制对端点的访问。

    一些额外信息:应用程序中的控制器类不扩展任何其他类,并且仅使用@Path@Stateless注释进行修饰。

    更新1

    我遵循使用过滤器的方法(如下面的Bogdan所建议的)。我创建了一个捕获所有请求的过滤器。应用程序正确启动。调用过滤器类中的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名称绑定的方法,正如CássioMazzochiMolin所建议的那样。

    我创建了界面:

    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>
    

    代码全部编译好,应用程序启动正常。

    但是当我访问端点(应该被过滤器捕获的端点)时,则不执行过滤器代码(我从未在过滤器方法中看到print语句),并且端点只是正常执行。

    由于某种原因,过滤器未捕获请求。

    我不知道问题是否与端点是POST的事实有关。或者,可能JAX-RS没有找到过滤器类,它用@provider修饰,但我不知道是否需要以任何其他方式注册过滤器。

2 个答案:

答案 0 :(得分:4)

我认为您不会找到任何开箱即用解决方案。

使用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服务不可用

     

由于a,服务器当前无法处理请求     临时超载维护服务器。暗示     这是一个临时条件,将在之后得到缓解     有些延迟。如果已知,延迟的长度可以用a表示     Retry-After标题。如果没有给出Retry-After,客户端应该是     处理响应,就像处理500响应一样。

     

注意:503状态代码的存在并不意味着a     服务器必须在变得过载时使用它。有些服务器可能希望     简单地拒绝连接

将过滤器绑定到端点

要将过滤器绑定到端点方法或类,请使用上面定义的@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进行了注释。

答案 1 :(得分:3)

您可以激活所有端点,但在收到密钥之前禁止访问。您可以通过在所有端点前放置Servlet Filter来查找激活密钥后设置的标记。

如果设置了标志(意味着传入密钥已成功设置),则允许访问端点,否则从过滤器返回某种状态(401或403)。如果你在内存或某个地方快速设置该标志,那么过滤器的性能开销应该小到可以忽略。