Недавно я столкнулся с очень интересной проблемой, которая, как мне казалось, займет у меня всего пару минут: защита служб 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", "a@b.com" ) ) .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/peopleHTTP/1.1 401 Full authentication is required to access this resourceWWW-Authenticate: Basic realm="Realm"Cache-Control: must-revalidate,no-cache,no-storeContent-Type: text/html; charset=ISO-8859-1Content-Length: 339Server: 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/peopleHTTP/1.1 200 OKDate: Sun, 28 Sep 2014 20:07:35 GMTContent-Type: application/jsonContent-Length: 65Server: Jetty(9.2.2.v20140723)[{"firstName":"Tom","lastName":"Tommyknocker","email":"a@b.com"}] |
Отлично, это работает как шарм! Оказывается, это действительно очень легко. Кроме того, как было упомянуто ранее, аутентификация в памяти может быть заменена службой подробностей пользователя, вот пример, как это можно сделать:
|
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} . |