Enterprise Java

Java EE 7 / JAX-RS 2.0 – CORS on REST

Java EE REST application usually works well out of the box on a development machine where all server side resources and client side UIs point to “localhost” or 127.0.0.1. But when it comes to cross domain deployment (when the REST client is no longer on the same domain as the server that host the REST APIs), some work-around is required. This article is about how to make Cross Domain or better known as Cross-origin Resource Sharing a.k.a CORS work when it comes to Java EE 7 / JAX-RS 2.0 REST APIs. It is not the intention of this article to discuss about browser and other security related mechanisms, you may find this on other websites; but what we truly want to achieve here is again, to get things working as soon as possible.
 
 

What is the Problem?

Demo Java EE 7 (JAX-RS 2.0) REST Service

In this article, I’ll just code a a simple Java EE 7 JAX-RS 2.0 based REST web service and client for demo purpose.

Here, I’ll define an interface annotating it with the url path of the REST service, along with the accepted HTTP methods and MIME Type for the HTTP response.

Codes for RESTCorsDemoResourceProxy.java:

package com.developerscrappad.intf;
 
import java.io.Serializable;
import javax.ejb.Local;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
 
@Local
@Path( "rest-cors-demo" )
public interface RESTCorsDemoResourceProxy extends Serializable {
 
    @GET
    @Path( "get-method" )
    @Produces( MediaType.APPLICATION_JSON )
    public Response getMethod();
 
    @PUT
    @Path( "put-method" )
    @Produces( MediaType.APPLICATION_JSON )
    public Response putMethod();
 
    @POST
    @Path( "post-method" )
    @Produces( MediaType.APPLICATION_JSON )
    public Response postMethod();
 
    @DELETE
    @Path( "delete-method" )
    @Produces( MediaType.APPLICATION_JSON )
    public Response deleteMethod();
}

Codes for RESTCorsDemoResource.java:

package com.developerscrappad.business;
 
import com.developerscrappad.intf.RESTCorsDemoResourceProxy;
import javax.ejb.Stateless;
import javax.json.Json;
import javax.json.JsonObject;
import javax.json.JsonObjectBuilder;
import javax.ws.rs.core.Response;
 
@Stateless( name = "RESTCorsDemoResource", mappedName = "ejb/RESTCorsDemoResource" )
public class RESTCorsDemoResource implements RESTCorsDemoResourceProxy {
 
    @Override
    public Response getMethod() {
        JsonObjectBuilder jsonObjBuilder = Json.createObjectBuilder();
        jsonObjBuilder.add( "message", "get method ok" );
 
        JsonObject jsonObj = jsonObjBuilder.build();
 
        return Response.status( Response.Status.OK ).entity( jsonObj.toString() ).build();
    }
 
    @Override
    public Response putMethod() {
        JsonObjectBuilder jsonObjBuilder = Json.createObjectBuilder();
        jsonObjBuilder.add( "message", "get method ok" );
 
        JsonObject jsonObj = jsonObjBuilder.build();
 
        return Response.status( Response.Status.ACCEPTED ).entity( jsonObj.toString() ).build();
    }
 
    @Override
    public Response postMethod() {
        JsonObjectBuilder jsonObjBuilder = Json.createObjectBuilder();
        jsonObjBuilder.add( "message", "post method ok" );
 
        JsonObject jsonObj = jsonObjBuilder.build();
 
        return Response.status( Response.Status.CREATED ).entity( jsonObj.toString() ).build();
    }
 
    @Override
    public Response deleteMethod() {
        JsonObjectBuilder jsonObjBuilder = Json.createObjectBuilder();
        jsonObjBuilder.add( "message", "delete method ok" );
 
        JsonObject jsonObj = jsonObjBuilder.build();
 
        return Response.status( Response.Status.ACCEPTED ).entity( jsonObj.toString() ).build();
    }
}

The codes in RESTCorsDemoResource is straight forward but please bare in mind that this is just a demo application and it has no valid purpose in its business logic. The RESTCorsDemoResource class implements the method signatures defined in the interface RESTCorsDemoResourceProxy. It has several methods which process incoming HTTP request through specific HTTP methods like GET, PUT, POST and DELETE, and at the end of the method, returns a simple JSON message when the process is done.

Not forgetting the web.xml below which tells the app server to treat it as a REST API call for any incoming HTTP request when the path detects “/rest-api/*” (e.g. http://<host>:<port>/AppName/rest-api/get-method/).

Contents in web.xml:



 
    
    
        javax.ws.rs.core.Application
        1
    
    
        javax.ws.rs.core.Application
        /rest-api/*
    
 

Deployment

Let’s package the above in a war file say RESTCorsDemo.war and deploy it to a Java EE 7 compatible app server. On my side, I’m running this on Glassfish 4.0 with default settings, which resides in machine with the public domain developerscrappad.com

Once deployed, the URLs to the REST services should be as the below:

MethodREST URL
RESTCorsDemoResourceProxy.getMethod()http://developerscrappad.com/RESTCorsDemo/rest-api/rest-cors-demo/get-method/
RESTCorsDemoResourceProxy.postMethod()http://developerscrappad.com/RESTCorsDemo/rest-api/rest-cors-demo/post-method/
RESTCorsDemoResourceProxy.putMethod()http://developerscrappad.com/RESTCorsDemo/rest-api/rest-cors-demo/put-method/
RESTCorsDemoResourceProxy.deleteMethod()http://developerscrappad.com/RESTCorsDemo/rest-api/rest-cors-demo/delete-method/

 

HTML REST Client

On my local machine, I’ll just create a simple HTML page to invoke the deployed REST server resources with the below:

Codes for rest-test.html:

<!DOCTYPE html>
<html>
    <head>
        <title>REST Tester</title>
        <meta charset="UTF-8">
    </head>
    <body>
        <div id="logMsgDiv"></div>
 
        <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
        <script type="text/javascript">
            var $ = jQuery.noConflict();
 
            $.ajax( {
                cache: false,
                crossDomain: true,
                dataType: "json",
                url: "http://developerscrappad.com:8080/RESTCorsDemo/rest-api/rest-cors-demo/get-method/",
                type: "GET",
                success: function( jsonObj, textStatus, xhr ) {
                    var htmlContent = $( "#logMsgDiv" ).html( ) + "<p>" + jsonObj.message + "</p>";
                    $( "#logMsgDiv" ).html( htmlContent );
                },
                error: function( xhr, textStatus, errorThrown ) {
                    console.log( "HTTP Status: " + xhr.status );
                    console.log( "Error textStatus: " + textStatus );
                    console.log( "Error thrown: " + errorThrown );
                }
            } );
 
            $.ajax( {
                cache: false,
                crossDomain: true,
                dataType: "json",
                url: "http://developerscrappad.com:8080/RESTCorsDemo/rest-api/rest-cors-demo/post-method/",
                type: "POST",
                success: function( jsonObj, textStatus, xhr ) {
                    var htmlContent = $( "#logMsgDiv" ).html( ) + "<p>" + jsonObj.message + "</p>";
                    $( "#logMsgDiv" ).html( htmlContent );
                },
                error: function( xhr, textStatus, errorThrown ) {
                    console.log( "HTTP Status: " + xhr.status );
                    console.log( "Error textStatus: " + textStatus );
                    console.log( "Error thrown: " + errorThrown );
                }
            } );
 
            $.ajax( {
                cache: false,
                crossDomain: true,
                dataType: "json",
                url: "http://developerscrappad.com:8080/RESTCorsDemo/rest-api/rest-cors-demo/put-method/",
                type: "PUT",
                success: function( jsonObj, textStatus, xhr ) {
                    var htmlContent = $( "#logMsgDiv" ).html( ) + "<p>" + jsonObj.message + "</p>";
                    $( "#logMsgDiv" ).html( htmlContent );
                },
                error: function( xhr, textStatus, errorThrown ) {
                    console.log( "HTTP Status: " + xhr.status );
                    console.log( "Error textStatus: " + textStatus );
                    console.log( "Error thrown: " + errorThrown );
                }
            } );
 
            $.ajax( {
                cache: false,
                crossDomain: true,
                dataType: "json",
                url: "http://developerscrappad.com:8080/RESTCorsDemo/rest-api/rest-cors-demo/delete-method/",
                type: "DELETE",
                success: function( jsonObj, textStatus, xhr ) {
                    var htmlContent = $( "#logMsgDiv" ).html( ) + "<p>" + jsonObj.message + "</p>";
                    $( "#logMsgDiv" ).html( htmlContent );
                },
                error: function( xhr, textStatus, errorThrown ) {
                    console.log( "HTTP Status: " + xhr.status );
                    console.log( "Error textStatus: " + textStatus );
                    console.log( "Error thrown: " + errorThrown );
                }
            } );
        </script>
    </body>
</html>

Here, I’m using jQuery’s ajax object for REST Services call with the defined option. The purpose of the rest-test.html is to invoke the REST Service URLs with the appropriate HTTP method and obtain the response as JSON result for processing later. I won’t go into detail here but in case if you like to know more about the $.ajax call options available, you may visit jQuery’s documentation site on this.

What happens when we run rest-test.html?

When I run the rest-test.html file on my Firefox browser, equip with the Firebug plugin, the below screen shots are what I get.

Screen Shot: Firebug Console Tab Result
Screen Shot: Firebug Console Tab Result

Screen Shot: Firebug Net Tab Result
Screen Shot: Firebug Net Tab Result

As you can see, when I check on the console tab, both the “/rest-api/rest-cors-demo/get-method/” and the “/rest-api/rest-cors-demo/post-method/” returned the right HTTP Status, but I can be absolutely sure that the method wasn’t executed on the remote Glassfish app server, the REST service calls were just bypassed, on the rest-test.html client, it just went straight to the $.ajax error callbacks. What about the “/rest-api/rest-cors-demo/put-method/” and the “/rest-api/rest-cors-demo/delete-method/“, when I check on the Firebug Net Tab as shown on one of the screen shots, the browser sent a Preflight Request by firing OPTIONS as the HTTP Method instead of the PUT and the DELETE. This phenomenon relates to both server side and browser security; I have compiled some other websites relating this at the bottom of the page.

How To Make CORS Works in Java EE 7 / JAX-RS 2.0 (Through Interceptors)

In order to make cross domain calls or simply known as CORS work on both the client and the server side REST resource, I have created two JAX-RS 2.0 interceptor classes, one implementing the ContainerRequestFilter and another implementing the ContainerResponseFilter.

Additional HTTP Headers in ContainerResponseFilter

The browser will require some additional HTTP headers to be responded back to it to further verify whether the server side resources allow cross domain / cross-origin resource sharing and to which level of security or limitation it permits. These are the headers which work pretty well out of the box for enabling CORS.

Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, DELETE, PUT

These sets of of additional HTTP Headers that could be included as part of the HTTP response when it goes back to the browser by having it included in a class which implements ContainerResponseFilter.

** But do take note: Having “Access-Control-Allow-Origin: *” will allow all calls to be accepted regardless of the location of the client. There are ways for you to further restrict this is you only want the server side to permit REST service calls from only a specific domain. Please check out the related articles at the bottom of the page.

Codes for RESTCorsDemoResponseFilter.java:

package com.developerscrappad.filter;
 
import java.io.IOException;
import java.util.logging.Logger;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.container.PreMatching;
import javax.ws.rs.ext.Provider;
 
@Provider
@PreMatching
public class RESTCorsDemoResponseFilter implements ContainerResponseFilter {
 
    private final static Logger log = Logger.getLogger( RESTCorsDemoResponseFilter.class.getName() );
 
    @Override
    public void filter( ContainerRequestContext requestCtx, ContainerResponseContext responseCtx ) throws IOException {
        log.info( "Executing REST response filter" );
 
        responseCtx.getHeaders().add( "Access-Control-Allow-Origin", "*" );
        responseCtx.getHeaders().add( "Access-Control-Allow-Credentials", "true" );
        responseCtx.getHeaders().add( "Access-Control-Allow-Methods", "GET, POST, DELETE, PUT" );
    }
}

Dealing With Browser Preflight Request HTTP Method: OPTIONS

The RESTCorsDemoResponseFilter class which implements ContainerResponseFilter only solved part of the issue. We still have to deal with the browser’s pre-flight request for the PUT and the DELETE HTTP methods. The underlying pre-flight request mechanism of most of the popular browsers work in such a way that they send a request with OPTIONS as the HTTP Method just to test the waters. If the server side resource acknowledges the path url of the request and allows PUT or DELETE HTTP Method to be accepted for processing, the server side will typically have to send an HTTP Status 200 (OK) response (or any sort of 20x HTTP Status) back to the browser before the browser sends the actual request as HTTP Method PUT or DELETE after that. However, this mechanism would have to implemented manually by the developer. So, I have implemented a new class by the name of RESTCorsDemoRequestFilter which implements ContainerRequestFilter shown at the below for this mechanism.

Codes for RESTCorsDemoRequestFilter.java:

package com.developerscrappad.filter;
 
import java.io.IOException;
import java.util.logging.Logger;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.PreMatching;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;
 
@Provider
@PreMatching
public class RESTCorsDemoRequestFilter implements ContainerRequestFilter {
 
    private final static Logger log = Logger.getLogger( RESTCorsDemoRequestFilter.class.getName() );
 
    @Override
    public void filter( ContainerRequestContext requestCtx ) throws IOException {
        log.info( "Executing REST request filter" );
 
        // When HttpMethod comes as OPTIONS, just acknowledge that it accepts...
        if ( requestCtx.getRequest().getMethod().equals( "OPTIONS" ) ) {
            log.info( "HTTP Method (OPTIONS) - Detected!" );
 
            // Just send a OK signal back to the browser
            requestCtx.abortWith( Response.status( Response.Status.OK ).build() );
        }
    }
}

The Result

After the RESTCorsDemoResponseFilter and the RESTCorsDemoRequestFilter are included in the application and deployed. I then rerun rest-test.html on my browser again. As a result, all the HTTP requests with different HTTP Methods of GET, POST, PUT and DELETE from a different location handled very well by the JAX-RS 2.0 application. The screen shots below are the successful HTTP requests made by my browser. These results of Firebug Console and NET Tab are what should be expected:

Screen Shot: Firebug Console Tab
Screen Shot: Firebug Console Tab

Screen Shot: Firebug Net Tab
Screen Shot: Firebug Net Tab

 

Final Words

JAX-RS 2.0 Interceptors are very handy when it comes to intercepting REST related request and response for such scenario like enabling CORS. If you are using specific implementation of REST library for your Java project e.g. Jersey or RESTEasy, do check out how request and response interceptors are to be specifically implemented, apply the above technique and you should be able to get the same result. The same principles are pretty much the same.

Well, hopefully this article will help you in solving cross domain or CORS issues on your Java EE 7 / JAX-RS 2.0 REST project.

Thank you for reading.

Related Articles:

Max Lam

Born and currently resides in Malaysia, a seasoned Java developer whom had held positions as Senior Developer, Consultant and Technical Architect in various enterprise software development companies.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button