Статьи

Embedded Jetty и Apache CXF: безопасные службы REST с помощью Spring Security

Недавно я столкнулся с очень интересной проблемой, которая, как мне казалось, займет у меня всего пару минут: защита служб Apache CXF (текущий выпуск 3.0.1 ) / JAX-RS REST с помощью Spring Security (текущая стабильная версия 3.2.5 ) в приложение работает внутри встроенного контейнера Jetty (текущий выпуск 9.2 ). В конце концов, это оказывается очень легко, когда вы понимаете, как все работает вместе, и узнаете тонкие внутренние детали. Этот пост будет пытаться раскрыть это.

В нашем примере приложения будет представлен простой сервис JAX-RS / REST для управления людьми. Однако мы не хотим, чтобы всем было разрешено делать это, поэтому для доступа к нашей конечной точке, развернутой по адресу http: // localhost: 8080 / api / rest / people, потребуется базовая аутентификация HTTP . Давайте посмотрим на класс PeopleRestService :

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

Как вы можете видеть из приведенного выше фрагмента, ничто не указывает на тот факт, что эта служба REST защищена, всего лишь пара знакомых аннотаций JAX-RS .

Теперь давайте объявим желаемую конфигурацию безопасности, следуя отличной документации Spring Security . Существует множество способов настройки Spring Security, но мы собираемся показать два из них: использование аутентификации в памяти и использование службы сведений о пользователе, оба построены на основе WebSecurityConfigurerAdapter . Давайте начнем с аутентификации в памяти, так как она самая простая:

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

В приведенном выше фрагменте определены два пользователя: пользователь с ролью USER и администратор с ролями USER , ADMIN . Мы также защищаем все URL ( / ** ), устанавливая политику авторизации, чтобы разрешить доступ только пользователям с ролью USER . Будучи лишь частью конфигурации приложения, давайте подключим его к классу AppConfig с помощью аннотации @Import .

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

На данный момент у нас есть все части, кроме самого интересного: кода, который запускает встроенный экземпляр Jetty и создает надлежащие отображения сервлетов, слушателей, передавая конфигурацию, которую мы создали.

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

Большая часть кода не требует каких-либо объяснений, кроме части фильтра. Это то, что я имел в виду под тонкими внутренними деталями: DelegatingFilterProxy должен быть настроен с именем фильтра, которое должно быть точно springSecurityFilterChain , как его называет Spring Security . При этом настроенные нами правила безопасности будут применяться к любому служебному вызову JAX-RS (фильтр безопасности выполняется перед сервлетом Apache CXF ), требующим полной аутентификации. Давайте быстро проверим это, собрав и запустив проект:

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

Вызов HTTP GET без указания имени пользователя и пароля не выполняется и возвращает код состояния HTTP 401 .

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

Тот же HTTP- вызов GET с предоставленными именем пользователя и паролем возвращает успешный ответ (с некоторым JSON, сгенерированным сервером).

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

Отлично, это работает как шарм! Оказывается, это действительно очень легко. Кроме того, как было упомянуто ранее, аутентификация в памяти может быть заменена службой подробностей пользователя, вот пример, как это можно сделать:

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

Замена @Import (InMemorySecurityConfig.class) на @Import (UserDetailsSecurityConfig.class) в классе AppConfig приводит к одинаковым результатам, поскольку обе конфигурации безопасности определяют идентичные наборы пользователей и их роли.

Надеюсь, этот пост сэкономит вам время и даст хорошую отправную точку, поскольку Apache CXF и Spring Security очень хорошо уживаются под зонтиком Jetty !

  • Полный исходный код доступен на GitHub .