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; |
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; |
10 |
public
class PeopleRestService { |
11 |
@Produces ( {
"application/json"
} ) |
13 |
public
JsonArray getPeople() { |
14 |
return
Json.createArrayBuilder() |
15 |
.add( Json.createObjectBuilder() |
16 |
.add(
"firstName" ,
"Tom" ) |
17 |
.add(
"lastName" ,
"Tommyknocker" ) |
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; |
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; |
14 |
@EnableGlobalMethodSecurity ( securedEnabled =
true ) |
15 |
public
class InMemorySecurityConfig extends
WebSecurityConfigurerAdapter { |
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"
); |
24 |
protected
void configure( HttpSecurity http )
throws Exception { |
25 |
http.httpBasic().and() |
26 |
.sessionManagement().sessionCreationPolicy( SessionCreationPolicy.STATELESS ).and() |
27 |
.authorizeRequests().antMatchers( "/**" ).hasRole(
"USER" ); |
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; |
03 |
import
java.util.Arrays; |
05 |
import
javax.ws.rs.ext.RuntimeDelegate; |
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; |
16 |
import
com.example.rs.JaxRsApiApplication; |
17 |
import
com.example.rs.PeopleRestService; |
20 |
@Import ( InMemorySecurityConfig. class
) |
21 |
public
class AppConfig { |
22 |
@Bean ( destroyMethod =
"shutdown" ) |
23 |
public
SpringBus cxf() { |
24 |
return
new SpringBus(); |
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(); |
37 |
public
JaxRsApiApplication jaxRsApiApplication() { |
38 |
return
new JaxRsApiApplication(); |
42 |
public
PeopleRestService peopleRestService() { |
43 |
return
new PeopleRestService(); |
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.
03 |
import
java.util.EnumSet; |
05 |
import
javax.servlet.DispatcherType; |
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; |
16 |
import
com.example.config.AppConfig; |
18 |
public
class Starter { |
19 |
public
static void
main( final String[] args )
throws Exception { |
20 |
Server server =
new Server(
8080 ); |
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() ); |
29 |
context.setInitParameter(
"contextClass" , AnnotationConfigWebApplicationContext. class .getName() ); |
30 |
context.setInitParameter(
"contextConfigLocation" , AppConfig. class .getName() ); |
34 |
new
FilterHolder( new
DelegatingFilterProxy( "springSecurityFilterChain"
) ), |
35 |
"/*" , EnumSet.allOf( DispatcherType. class
) |
38 |
server.setHandler( context ); |
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:
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.
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 |
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: |
4 |
Date: Sun, 28
Sep 2014
20 : 07 : 35
GMT |
5 |
Content-Type: application/json |
7 |
Server: Jetty( 9.2 . 2 .v20140723) |
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; |
03 |
import
java.util.Arrays; |
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; |
22 |
@EnableGlobalMethodSecurity (securedEnabled =
true ) |
23 |
public
class UserDetailsSecurityConfig extends
WebSecurityConfigurerAdapter { |
25 |
public
void configureGlobal(AuthenticationManagerBuilder auth)
throws Exception { |
26 |
auth.userDetailsService( userDetailsService() ); |
30 |
public
UserDetailsService userDetailsService() { |
31 |
return
new UserDetailsService() { |
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 , |
38 |
new
SimpleGrantedAuthority( "ROLE_USER"
), |
39 |
new
SimpleGrantedAuthority( "ROLE_ADMIN"
) |
42 |
}
else if
( username.equals( "user"
) ) { |
43 |
return
new User( username,
"password" , true ,
true , true ,
true , |
45 |
new
SimpleGrantedAuthority( "ROLE_USER"
) |
56 |
protected
void configure( HttpSecurity http )
throws Exception { |
59 |
.sessionManagement().sessionCreationPolicy( SessionCreationPolicy.STATELESS ).and() |
60 |
.authorizeRequests().antMatchers( "/**" ).hasRole(
"USER" ); |
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.