Недавно я столкнулся с очень интересной проблемой, которая, как я думал, займет у меня всего пару минут: защита служб 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 :
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 . Давайте начнем с аутентификации в памяти, так как она самая простая:
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 .
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 и создает надлежащие отображения сервлетов, слушателей, передавая конфигурацию, которую мы создали.
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 ), требующим полной аутентификации. Давайте быстро проверим это, собрав и запустив проект:
mvn clean package java -jar target/jax-rs-2.0-spring-security-0.0.1-SNAPSHOT.jar
Вызов HTTP GET без указания имени пользователя и пароля не выполняется и возвращает код состояния HTTP 401 .
> 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, сгенерированным сервером).
> 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":"a@b.com"}]
Отлично, это работает как шарм! Оказывается, это действительно очень легко. Кроме того, как было упомянуто ранее, аутентификация в памяти может быть заменена службой подробностей пользователя, вот пример, как это можно сделать:
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 .