最近,我遇到了一个非常有趣的问题,我认为这只花了我几分钟就解决了:在Windows Server 2003中使用Spring Security (当前稳定版本3.2.5 )保护Apache CXF (当前版本3.0.1 )/ JAX-RS REST服务。在嵌入式Jetty容器(当前版本9.2 )中运行的应用程序。 最后,一旦您了解了事物如何协同工作并了解了细微的内在细节,这将变得非常容易。 这篇博客文章将试图揭示这一点。
我们的示例应用程序将公开一个简单的JAX-RS / REST服务来管理人员。 但是,我们不希望所有人都这样做,因此需要HTTP基本身份验证才能访问部署在http:// localhost:8080 / api / rest / people的端点。 让我们看一下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" );
}
}
在上面有该段两个用户定义: 用户与角色用户和管理员与用户的角色,ADMIN。 我们还通过将授权策略设置为仅允许访问角色为USER的用户来保护所有URL( / ** )。 作为应用程序配置的一部分,让我们使用@Import批注将其插入AppConfig类。
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实例并创建正确的servlet映射,侦听器,传递我们创建的配置的代码。
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 servlet之前执行),需要完全认证。 让我们通过构建和运行项目来快速检查:
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" );
}
}
将AppConfig类中的@Import(InMemorySecurityConfig.class)替换为@Import( UserDetailsSecurityConfig.class)会得到相同的结果,因为两个安全配置都定义了相同的用户及其角色集。
我希望,此博客文章可以节省您一些时间,并为您提供一个良好的起点,因为Apache CXF和Spring Security在Jetty的保护下相处得很好!
- 完整的源代码可在GitHub上获得 。