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

3 篇文章 0 订阅


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:

01package com.example.rs;
02 
03import javax.json.Json;
04import javax.json.JsonArray;
05import javax.ws.rs.GET;
06import javax.ws.rs.Path;
07import javax.ws.rs.Produces;
08 
09@Path( "/people" )
10public 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", "a@b.com" ) )
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:

01package com.example.config;
02 
03import org.springframework.beans.factory.annotation.Autowired;
04import org.springframework.context.annotation.Configuration;
05import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
06import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
07import org.springframework.security.config.annotation.web.builders.HttpSecurity;
08import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
09import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
10import org.springframework.security.config.http.SessionCreationPolicy;
11 
12@Configuration
13@EnableWebSecurity
14@EnableGlobalMethodSecurity( securedEnabled = true )
15public 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.

01package com.example.config;
02 
03import java.util.Arrays;
04 
05import javax.ws.rs.ext.RuntimeDelegate;
06 
07import org.apache.cxf.bus.spring.SpringBus;
08import org.apache.cxf.endpoint.Server;
09import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
10import org.apache.cxf.jaxrs.provider.jsrjsonp.JsrJsonpProvider;
11import org.springframework.context.annotation.Bean;
12import org.springframework.context.annotation.Configuration;
13import org.springframework.context.annotation.DependsOn;
14import org.springframework.context.annotation.Import;
15 
16import com.example.rs.JaxRsApiApplication;
17import com.example.rs.PeopleRestService;
18 
19@Configuration
20@Import( InMemorySecurityConfig.class )
21public 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.

01package com.example;
02 
03import java.util.EnumSet;
04 
05import javax.servlet.DispatcherType;
06 
07import org.apache.cxf.transport.servlet.CXFServlet;
08import org.eclipse.jetty.server.Server;
09import org.eclipse.jetty.servlet.FilterHolder;
10import org.eclipse.jetty.servlet.ServletContextHandler;
11import org.eclipse.jetty.servlet.ServletHolder;
12import org.springframework.web.context.ContextLoaderListener;
13import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
14import org.springframework.web.filter.DelegatingFilterProxy;
15 
16import com.example.config.AppConfig;
17 
18public 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:

1mvn clean package  
2java -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 
3HTTP/1.1 401 Full authentication is required to access this resource
4WWW-Authenticate: Basic realm="Realm"
5Cache-Control: must-revalidate,no-cache,no-store
6Content-Type: text/html; charset=ISO-8859-1
7Content-Length: 339
8Server: 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 
3HTTP/1.1 200 OK
4Date: Sun, 28 Sep 2014 20:07:35 GMT
5Content-Type: application/json
6Content-Length: 65
7Server: Jetty(9.2.2.v20140723)
8 
9[{"firstName":"Tom","lastName":"Tommyknocker","email":"a@b.com"}]

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:

01package com.example.config;
02 
03import java.util.Arrays;
04 
05import org.springframework.beans.factory.annotation.Autowired;
06import org.springframework.context.annotation.Bean;
07import org.springframework.context.annotation.Configuration;
08import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
09import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
10import org.springframework.security.config.annotation.web.builders.HttpSecurity;
11import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
12import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
13import org.springframework.security.config.http.SessionCreationPolicy;
14import org.springframework.security.core.authority.SimpleGrantedAuthority;
15import org.springframework.security.core.userdetails.User;
16import org.springframework.security.core.userdetails.UserDetails;
17import org.springframework.security.core.userdetails.UserDetailsService;
18import org.springframework.security.core.userdetails.UsernameNotFoundException;
19 
20@Configuration
21@EnableWebSecurity
22@EnableGlobalMethodSecurity(securedEnabled = true)
23public 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.
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值