Embedded Jetty and Apache CXF: secure REST services with Spring Security



http://www.javacodegeeks.com/2014/09/embedded-jetty-and-apache-cxf-secure-rest-services-with-spring-security.html

Recently I run into very interesting problem which I thought would take me just a couple of minutes to solve: protecting Apache CXF (current release 3.0.1)/ JAX-RS REST services with Spring Security (current stable version 3.2.5) in the application running inside embedded Jetty container (current release 9.2). At the end, it turns out to be very easy, once you understand how things work together and known subtle intrinsic details. This blog post will try to reveal that.

Our example application is going to expose a simple JAX-RS / REST service to manage people. However, we do not want everyone to be allowed to do that so the HTTP basic authentication will be required in order to access our endpoint, deployed at http://localhost:8080/api/rest/people. Let us take a look on the PeopleRestService class:

01 package com.example.rs;
02  
03 import javax.json.Json;
04 import javax.json.JsonArray;
05 import javax.ws.rs.GET;
06 import javax.ws.rs.Path;
07 import javax.ws.rs.Produces;
08  
09 @Path( "/people" )
10 public class PeopleRestService {
11     @Produces( { "application/json" } )
12     @GET
13     public JsonArray getPeople() {
14         return Json.createArrayBuilder()
15             .add( Json.createObjectBuilder()
16                 .add( "firstName", "Tom" )
17                 .add( "lastName", "Tommyknocker" )
18                 .add( "email", "[email protected]" ) )
19             .build();
20     }
21 }

As you can see in the snippet above, nothing is pointing out to the fact that this REST service is secured, just couple of familiar JAX-RS annotations.

Now, let us declare the desired security configuration following excellent Spring Security documentation. There are many ways to configure Spring Security but we are going to show off two of them: using in-memory authentication and using user details service, both built on top of WebSecurityConfigurerAdapter. Let us start with in-memory authentication as it is the simplest one:

01 package com.example.config;
02  
03 import org.springframework.beans.factory.annotation.Autowired;
04 import org.springframework.context.annotation.Configuration;
05 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
06 import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
07 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
08 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
09 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
10 import org.springframework.security.config.http.SessionCreationPolicy;
11  
12 @Configuration
13 @EnableWebSecurity
14 @EnableGlobalMethodSecurity( securedEnabled = true )
15 public class InMemorySecurityConfig extends WebSecurityConfigurerAdapter {
16     @Autowired
17     public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
18         auth.inMemoryAuthentication()
19             .withUser( "user" ).password( "password" ).roles( "USER" ).and()
20             .withUser( "admin" ).password( "password" ).roles( "USER", "ADMIN" );
21     }
22  
23     @Override
24     protected void configure( HttpSecurity http ) throws Exception {
25         http.httpBasic().and()
26             .sessionManagement().sessionCreationPolicy( SessionCreationPolicy.STATELESS ).and()
27             .authorizeRequests().antMatchers("/**").hasRole( "USER" );
28     }
29 }

In the snippet above there two users defined: user with the role USER and admin with the roles USER, ADMIN. We also protecting all URLs (/**) by setting authorization policy to allow access only users with role USER. Being just a part of the application configuration, let us plug it into the AppConfig class using @Import annotation.

01 package com.example.config;
02  
03 import java.util.Arrays;
04  
05 import javax.ws.rs.ext.RuntimeDelegate;
06  
07 import org.apache.cxf.bus.spring.SpringBus;
08 import org.apache.cxf.endpoint.Server;
09 import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
10 import org.apache.cxf.jaxrs.provider.jsrjsonp.JsrJsonpProvider;
11 import org.springframework.context.annotation.Bean;
12 import org.springframework.context.annotation.Configuration;
13 import org.springframework.context.annotation.DependsOn;
14 import org.springframework.context.annotation.Import;
15  
16 import com.example.rs.JaxRsApiApplication;
17 import com.example.rs.PeopleRestService;
18  
19 @Configuration
20 @Import( InMemorySecurityConfig.class )
21 public class AppConfig {
22     @Bean( destroyMethod = "shutdown" )
23     public SpringBus cxf() {
24         return new SpringBus();
25     }
26   
27     @Bean @DependsOn ( "cxf" )
28     public Server jaxRsServer() {
29         JAXRSServerFactoryBean factory = RuntimeDelegate.getInstance().createEndpoint( jaxRsApiApplication(), JAXRSServerFactoryBean.class );
30         factory.setServiceBeans( Arrays.< Object >asList( peopleRestService() ) );
31         factory.setAddress( factory.getAddress() );
32         factory.setProviders( Arrays.< Object >asList( new JsrJsonpProvider() ) );
33         return factory.create();
34     }
35   
36     @Bean
37     public JaxRsApiApplication jaxRsApiApplication() {
38         return new JaxRsApiApplication();
39     }
40   
41     @Bean
42     public PeopleRestService peopleRestService() {
43         return new PeopleRestService();
44     
45 }

At this point we have all the pieces except the most interesting one: the code which runs embedded Jetty instance and creates proper servlet mappings, listeners, passing down the configuration we have created.

01 package com.example;
02  
03 import java.util.EnumSet;
04  
05 import javax.servlet.DispatcherType;
06  
07 import org.apache.cxf.transport.servlet.CXFServlet;
08 import org.eclipse.jetty.server.Server;
09 import org.eclipse.jetty.servlet.FilterHolder;
10 import org.eclipse.jetty.servlet.ServletContextHandler;
11 import org.eclipse.jetty.servlet.ServletHolder;
12 import org.springframework.web.context.ContextLoaderListener;
13 import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
14 import org.springframework.web.filter.DelegatingFilterProxy;
15  
16 import com.example.config.AppConfig;
17  
18 public class Starter {
19     public static void main( final String[] args ) throws Exception {
20         Server server = new Server( 8080 );
21            
22         // Register and map the dispatcher servlet
23         final ServletHolder servletHolder = new ServletHolder( new CXFServlet() );
24         final ServletContextHandler context = new ServletContextHandler();  
25         context.setContextPath( "/" );
26         context.addServlet( servletHolder, "/rest/*" ); 
27         context.addEventListener( new ContextLoaderListener() );
28     
29         context.setInitParameter( "contextClass", AnnotationConfigWebApplicationContext.class.getName() );
30         context.setInitParameter( "contextConfigLocation", AppConfig.class.getName() );
31     
32         // Add Spring Security Filter by the name
33         context.addFilter(
34             new FilterHolder( new DelegatingFilterProxy( "springSecurityFilterChain" ) ),
35                 "/*", EnumSet.allOf( DispatcherType.class )
36         );
37           
38         server.setHandler( context );
39         server.start();
40         server.join();
41     }
42 }

Most of the code does not require any explanation except the the filter part. This is what I meant by subtle intrinsic detail: the DelegatingFilterProxy should be configured with the filter name which must be exactly springSecurityFilterChain, as Spring Security names it. With that, the security rules we have configured are going to apply to any JAX-RS service call (the security filter is executed before the Apache CXF servlet), requiring the full authentication. Let us quickly check that by building and running the project:

1 mvn clean package  
2 java -jar target/jax-rs-2.0-spring-security-0.0.1-SNAPSHOT.jar

Issuing the HTTP GET call without providing username and password does not succeed and returns HTTP status code 401.

1 > curl -i http://localhost:8080/rest/api/people
2  
3 HTTP/1.1 401 Full authentication is required to access this resource
4 WWW-Authenticate: Basic realm="Realm"
5 Cache-Control: must-revalidate,no-cache,no-store
6 Content-Type: text/html; charset=ISO-8859-1
7 Content-Length: 339
8 Server: Jetty(9.2.2.v20140723)

The same HTTP GET call with username and password provided returns successful response (with some JSON generated by the server).

1 > curl -i -u user:password http://localhost:8080/rest/api/people
2  
3 HTTP/1.1 200 OK
4 Date: Sun, 28 Sep 2014 20:07:35 GMT
5 Content-Type: application/json
6 Content-Length: 65
7 Server: Jetty(9.2.2.v20140723)
8  
9 [{"firstName":"Tom","lastName":"Tommyknocker","email":"[email protected]"}]

Excellent, it works like a charm! Turns out, it is really very easy. Also, as it was mentioned before, the in-memory authentication could be replaced with user details service, here is an example how it could be done:

01 package com.example.config;
02  
03 import java.util.Arrays;
04  
05 import org.springframework.beans.factory.annotation.Autowired;
06 import org.springframework.context.annotation.Bean;
07 import org.springframework.context.annotation.Configuration;
08 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
09 import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
10 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
11 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
12 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
13 import org.springframework.security.config.http.SessionCreationPolicy;
14 import org.springframework.security.core.authority.SimpleGrantedAuthority;
15 import org.springframework.security.core.userdetails.User;
16 import org.springframework.security.core.userdetails.UserDetails;
17 import org.springframework.security.core.userdetails.UserDetailsService;
18 import org.springframework.security.core.userdetails.UsernameNotFoundException;
19  
20 @Configuration
21 @EnableWebSecurity
22 @EnableGlobalMethodSecurity(securedEnabled = true)
23 public class UserDetailsSecurityConfig extends WebSecurityConfigurerAdapter {
24     @Autowired
25     public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
26         auth.userDetailsService( userDetailsService() );
27     }
28      
29     @Bean
30     public UserDetailsService userDetailsService() {
31         return new UserDetailsService() {
32             @Override
33             public UserDetails loadUserByUsername( final String username )
34                     throws UsernameNotFoundException {
35                 if( username.equals( "admin" ) ) {
36                     return new User( username, "password", true, true, true, true,
37                         Arrays.asList(
38                             new SimpleGrantedAuthority( "ROLE_USER" ),
39                             new SimpleGrantedAuthority( "ROLE_ADMIN" )
40                         )
41                     );
42                 } else if ( username.equals( "user" ) ) {
43                     return new User( username, "password", true, true, true, true,
44                         Arrays.asList(
45                             new SimpleGrantedAuthority( "ROLE_USER" )
46                         )
47                     );
48                 }
49                      
50                 return null;
51             }
52         };
53     }
54  
55     @Override
56     protected void configure( HttpSecurity http ) throws Exception {
57         http
58            .httpBasic().and()
59            .sessionManagement().sessionCreationPolicy( SessionCreationPolicy.STATELESS ).and()
60            .authorizeRequests().antMatchers("/**").hasRole( "USER" );
61     }
62 }

Replacing the @Import( InMemorySecurityConfig.class ) with @Import( UserDetailsSecurityConfig.class ) in the AppConfig class leads to the same results, as both security configurations define the identical sets of users and their roles.

I hope, this blog post will save you some time and gives a good starting point, as Apache CXF and Spring Security are getting along very well under Jetty umbrella!

  • The complete source code is available on GitHub.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章