Search This Blog

Sunday, April 20, 2014

jersey/jaxrs 2.X example using spring JavaConfig and spring security

Introduction


I had blogged previously on using jersey/JAX-RS 2.0 when it was in a pre-release. Since then Jersey/JAX-RS 2.0 has released and undergone a few versions. As I have recently been working with the jersey 2.7-spring integration and spring security and figured I'd share an example.

Requirements


As is quite standard with me, I like to 'manufacture' requirements. The application being developed is a Notes application that allows one to perform CRUD operations for simple notes. So, for the Web Service being created, I have the following requirements:
  1. No XML- You get type safety during refactoring
  2. Spring Dependency Injection with Java Config. The official jersey/spring3 example is very nice but does not demonstrate Java Config usage.
  3. Jersey Should manage Resource classes an have Spring service objects injected into them
  4. Security should be enabled via Spring Security. Only users with the ROLE 'notesuser' should be able to create a note.

The Project

Look Ma, no XML

This example will use Servlet 3.X specification, so we eliminate web.xml. There are many ways one can eliminate XML using Servlet 3.X, but this example focuses on the usage of javax.servlet.ServletContainerInitializer. With a ServletContainerInitializer one can programatically register Servlets, Filters and Listeners thus giving you type safety that can be useful during refactoring. As we are in Spring Land, rather than utilize ServletContainerInitializer, an extension of Spring's WebApplicationInitializer is used as shown below:
@Order(Ordered.HIGHEST_PRECEDENCE) 
public class NotesApplicationInitializer implements WebApplicationInitializer {

  private static final String SPRING_SECURITY_FILTER_NAME = "springSecurityFilterChain";
  
  @Override public void onStartup(ServletContext ctx) throws ServletException {
    // Listeners
    ctx.addListener(ContextLoaderListener.class);

    ctx.setInitParameter(ContextLoader.CONTEXT_CLASS_PARAM,
      AnnotationConfigWebApplicationContext.class.getName());
    ctx.setInitParameter(ContextLoader.CONFIG_LOCATION_PARAM, SpringConfig.class.getName());

    // Register Spring Security Filter chain
    ctx.addFilter(SPRING_SECURITY_FILTER_NAME, DelegatingFilterProxy.class)
        .addMappingForUrlPatterns(
          EnumSet.<DispatcherType> of(DispatcherType.REQUEST, DispatcherType.FORWARD), false, "/*");

    // Register Jersey 2.0 servlet
    ServletRegistration.Dynamic servletRegistration = ctx.addServlet("Notes",
      ServletContainer.class.getName());
    
    servletRegistration.addMapping("/*");
    servletRegistration.setLoadOnStartup(1);
    servletRegistration.setInitParameter("javax.ws.rs.Application", NotesApplication.class.getName());
  }
}
The WebApplicationInitializer does the following:
  1. Sets up a Spring ContextLoaderListener
  2. Tells the Context loader the location of the Spring Java Config that is to be used for Services
  3. Registers the Spring Security Filter chain
  4. Registers the Jersey Servlet Container by providing it the NotesApplication as the class to use for Resource and Provider management
Of note is the fact that the NotesApplicationInitializer has been set to highest precedence as that will ensure that it is executed before any other WebApplicationInitializer provided by accompanying jars. If for example, the SpringWebApplicationInitializer from jerseyspring3.jar gets loaded, then it attempts to find a spring  applicationContext.xml and will fail as our example does not use spring xml style bean definitions.

Spring Dependency Injection

In conjunction with the registration of the Spring Java Config in the WebApplicationInitializer shown above, the Java Config enables the Services and Spring Security filter chain via the following:
@Configuration
@EnableNotesService
@EnableNotesSecurity
public class SpringConfig {
}
I have not delved into the details of the @Enable annotations but they are in line with Spring's style to import dependencies. For the scope of this example, you can assume that @EnableNotesSecurity results in the importing of the notes Java services and @EnableNotesSecurity imports the security configuration.

Jersey Should Manage the Resource Classes

I wanted all my JAX-RS resources managed by Jersey and not Spring. I did not want to annotate my JAX-RS classes with @Component + Classpath-scanning and/or have them defined in a Java Config which would then result in Spring managing them. All the Resources and relevant providers are registered with Jersey by extending the javax.ws.rs.core.Application class as shown below:
public class NotesApplication extends Application {
  
  @Override
  public Set<Class<?>> getClasses() {
    return ImmutableSet.<Class<?>>of(NotesResource.class, 
      HealthResource.class,
      NoteNotFoundExceptionMapper.class, LoggingFilter.class, AccessDeniedExeptionMapper.class);
  }
}
Note that the 'service' java classes, such as NotesService.java (managed by Spring) are made available via dependency injection into the NotesResource via Jersey's Spring-HK2 bridge.

Security Should be enabled Via Spring Security

For this example,I have very simple Spring Security filter chain set up that ensures a POST to create a Note can only be done by a user who has the ROLE 'notesuser' provided in a HTTP header while making the call. It is trivial, but hey, this is an example :-) The goal was to demonstrate the use of a @PreAuthorize annotation on the create a Note and how it can be made to work when Jersey manages the resource and not spring. The Notes resource create method looks like the following:
@Path("/notes")
@Produces({ MediaType.APPLICATION_XML })
@Consumes({ MediaType.APPLICATION_XML })
public class NotesResource {
  // Jersey object injection
  private final UriInfo uriInfo;
  
  // Spring object injection
  private final NotesService notesService;
  
  // Note that UriInfo is obtained from Jersey but the NotesService is a spring dependency
  @Inject
  public NotesResource(@Context UriInfo uriInfo, NotesService notesService) {
    this.uriInfo = uriInfo;
    this.notesService = notesService;
  }
  
  @POST
  @Loggable
  @PreAuthorize("hasRole('notesuser')") // Pre Authorize role that allows only notesuser to create the note
  public Response create(Note note) {
    LOG.debug("Creating a note:" + note);
    NoteResult result = createNote(note);
    
    return Response.created(result.getLocation()).entity(result).build();
  }
  .....
}
As the NotesResource is managed by Jersey, the only way I could figure to enforce the @PreAuthorize annotation was via AspectJ weaving of the resource class. The same is accomplished by adding the aspectJ maven plugin which is used during the build process to enhance the JAX-RS Resource classes:
 <plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>aspectj-maven-plugin</artifactId>
    <version>1.6</version>
    <configuration>
    ...
    <sources>
      <source>
        <basedir>${basedir}/src/main/java/com/welflex/notes/rest/resource</basedir>
        <includes>
           <include>**/*Resource.java</include>
        </includes>
      </source>
    </sources>
    ....
    <aspectLibraries>
      <aspectLibrary>
        <groupId>org.springframework.security</groupId>
          <artifactId>spring-security-aspects</artifactId>
      </aspectLibrary>
   </aspectLibraries>
   </configuration>
  <executions>
    <execution>
      <goals>       
        <goal>compile</goal>
      </goals>
    </execution>
  <executions>
  <dependencies>
    <dependency>
      <groupId>org.aspectj</groupId>
      <artifactId>aspectjrt</artifactId>
      <version>1.7.4</version>
    </dependency>
    <dependency>
      <groupId>org.aspectj</groupId>
      <artifactId>aspectjtools</artifactId>
      <version>1.7.4</version>
    </dependency>
  </dependencies>
<plugin>

Running the Example


The example is provided as a maven project. I am utilizing the concept of a maven 'bom' for the first time for managing the related jersey dependencies. Execute a 'mvn install' to see the example in action or execute a 'mvn jetty:run' on the web project to start the service. The example itself can be downloaded from here.

Client

The client will send the 'role' as a header parameter as shown below for creating a note:
  public NoteResult create(Note note, String role) {
    return webServiceClient.target(UriBuilder.fromUri(baseUri).path("/notes").build())
        .request(MediaType.APPLICATION_XML)
        .header("role", role)
        .post(Entity.entity(note, MediaType.APPLICATION_XML_TYPE), NoteResult.class);
  }

Integration Test

Integration test starts up a web container and executes the client operations. The following shows the spring-security role being honored:
  @Test(expected = ForbiddenException.class)
  public void roleAbsent() {
    Note note = new Note();
    note.setUserId("sacharya");
    note.setContent("Something to say");
    client.create(note, "Fake Role"); // Should throw a JAX-RS ForbiddenException
    fail("Should not have created a note as the role provided is not supported");
  }
Running the equivalent of the above with the role of 'notesuser' will allow the creation of the note. The life-cycle integration test in the attached example demonstrates the same succeeding. The example should be devoid of web.xml. If anyone coming across this blog has integrated Jersey/Spring/Spring-security in a different way, please share, would love to hear. That's all folks! Enjoy!

4 comments:

MacInfinity said...

Hey Sanjay, loved the post. I was reading this today thinking, this is great, then I saw it was you! Great post, using it now :)

Sanjay Acharya said...

Took me sometime to figure out who you are my friend :-) But did and glad it's helping you.

Anonymous said...

Thank you for your article. Could you please show the example of the integration test with successful authentication and use of protected Jersey resource?

Sharad said...

Hi Sanjay
I am trying to configure a Dispatcher servlet along with this code example and ending up duplicate context loaders

Need some help from you to initialize the app correctly