Недавно я столкнулся с очень интересной проблемой, которая, как я думал, займет у меня всего пару минут: защита служб 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 .