Недавно я столкнулся с очень интересной проблемой, которая, как мне казалось, займет у меня всего пару минут: защита служб 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" ) .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) |
Отлично, это работает как шарм! Оказывается, это действительно очень легко. Кроме того, как было упомянуто ранее, аутентификация в памяти может быть заменена службой подробностей пользователя, вот пример, как это можно сделать:
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 .
Ссылка: | Embedded Jetty и Apache CXF: защищенные REST-сервисы с Spring Security от нашего партнера по JCG Андрея Редько в блоге Андрея Редько {devmind} . |