This year started on a good note, another one of those “the deadline won’t change” / “skip all the red tape” / “Wild West” type of projects in which I got to figure out and implement some functionality using some relatively new libraries and tech for a change, well Spring 3 ain’t new but in the Java 5, weblogic 10(.01), Spring 2.5.6 slow corporate kind of world it is all relative.
Due to general time constraints I am not including too much “fluff” in this post, just the nitty gritty of creating and securing a Spring 3 , Spring WS 2 web service using multiple XSDs and LDAP security.
The Code:
The Service Endpoint: ExampleServiceEndpoint
This is the class that will be exposed as web service using the configuration later in the post.
package javaitzen.spring.ws; import org.springframework.ws.server.endpoint.annotation.Endpoint; import org.springframework.ws.server.endpoint.annotation.PayloadRoot; import org.springframework.ws.server.endpoint.annotation.RequestPayload; import org.springframework.ws.server.endpoint.annotation.ResponsePayload; import javax.annotation.Resource; @Endpoint public class ExampleServiceEndpoint { private static final String NAMESPACE_URI = "http://www.briandupreez.net"; /** * Autowire a POJO to handle the business logic @Resource(name = "businessComponent") private ComponentInterface businessComponent; */ public ExampleServiceEndpoint() { System.out.println(">> javaitzen.spring.ws.ExampleServiceEndpoint loaded."); } @PayloadRoot(localPart = "ProcessExample1Request", namespace = NAMESPACE_URI + "/example1") @ResponsePayload public Example1Response processExample1Request(@RequestPayload final Example1 request) { System.out.println(">> process example request1 ran."); return new Example1Response(); } @PayloadRoot(localPart = "ProcessExample2Request", namespace = NAMESPACE_URI + "/example2") @ResponsePayload public Example2Response processExample2Request(@RequestPayload final Example2 request) { System.out.println(">> process example request2 ran."); return new Example2Response(); } }
The Code: CustomValidationCallbackHandler
This was my bit of custom code I wrote to extend the AbstactCallbackHandler allowing us to use LDAP.
As per the comments in the CallbackHandler below, it’s probably a good idea to have a cache manager, something like Hazelcast or Ehcache to cache authenticated users, depending on security / performance considerations.
The Digest Validator below can just be used directly from the Sun library, I was just wanted to see how it worked.
package javaitzen.spring.ws; import com.sun.org.apache.xml.internal.security.exceptions.Base64DecodingException; import com.sun.xml.wss.impl.callback.PasswordValidationCallback; import com.sun.xml.wss.impl.misc.Base64; import org.springframework.beans.factory.InitializingBean; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.util.Assert; import org.springframework.ws.soap.security.callback.AbstractCallbackHandler; import javax.security.auth.callback.Callback; import javax.security.auth.callback.UnsupportedCallbackException; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.security.MessageDigest; import java.util.Properties; public class CustomValidationCallbackHandler extends AbstractCallbackHandler implements InitializingBean { private Properties users = new Properties(); private AuthenticationManager ldapAuthenticationManager; @Override protected void handleInternal(final Callback callback) throws IOException, UnsupportedCallbackException { if (callback instanceof PasswordValidationCallback) { final PasswordValidationCallback passwordCallback = (PasswordValidationCallback) callback; if (passwordCallback.getRequest() instanceof PasswordValidationCallback.DigestPasswordRequest) { final PasswordValidationCallback.DigestPasswordRequest digestPasswordRequest = (PasswordValidationCallback.DigestPasswordRequest) passwordCallback.getRequest(); final String password = users .getProperty(digestPasswordRequest .getUsername()); digestPasswordRequest.setPassword(password); passwordCallback .setValidator(new CustomDigestPasswordValidator()); } if (passwordCallback.getRequest() instanceof PasswordValidationCallback.PlainTextPasswordRequest) { passwordCallback .setValidator(new LDAPPlainTextPasswordValidator()); } } else { throw new UnsupportedCallbackException(callback); } } /** * Digest Validator. * This code is directly from the sun class, I was just curious how it worked. */ private class CustomDigestPasswordValidator implements PasswordValidationCallback.PasswordValidator { public boolean validate(final PasswordValidationCallback.Request request) throws PasswordValidationCallback.PasswordValidationException { final PasswordValidationCallback.DigestPasswordRequest req = (PasswordValidationCallback.DigestPasswordRequest) request; final String passwd = req.getPassword(); final String nonce = req.getNonce(); final String created = req.getCreated(); final String passwordDigest = req.getDigest(); final String username = req.getUsername(); if (null == passwd) return false; byte[] decodedNonce = null; if (null != nonce) { try { decodedNonce = Base64.decode(nonce); } catch (final Base64DecodingException bde) { throw new PasswordValidationCallback.PasswordValidationException(bde); } } String utf8String = ""; if (created != null) { utf8String += created; } utf8String += passwd; final byte[] utf8Bytes; try { utf8Bytes = utf8String.getBytes("utf-8"); } catch (final UnsupportedEncodingException uee) { throw new PasswordValidationCallback.PasswordValidationException(uee); } final byte[] bytesToHash; if (decodedNonce != null) { bytesToHash = new byte[utf8Bytes.length + decodedNonce.length]; for (int i = 0; i < decodedNonce.length; i++) bytesToHash[i] = decodedNonce[i]; for (int i = decodedNonce.length; i < utf8Bytes.length + decodedNonce.length; i++) bytesToHash[i] = utf8Bytes[i - decodedNonce.length]; } else { bytesToHash = utf8Bytes; } final byte[] hash; try { final MessageDigest sha = MessageDigest.getInstance("SHA-1"); hash = sha.digest(bytesToHash); } catch (final Exception e) { throw new PasswordValidationCallback.PasswordValidationException( "Password Digest could not be created" + e); } return (passwordDigest.equals(Base64.encode(hash))); } } /** * LDAP Plain Text validator. */ private class LDAPPlainTextPasswordValidator implements PasswordValidationCallback.PasswordValidator { /** * Validate the callback against the injected LDAP server. * Probably a good idea to have a cache manager - ehcache / hazelcast injected to cache authenticated users. * * @param request the callback request * @return true if login successful * @throws PasswordValidationCallback.PasswordValidationException * */ public boolean validate(final PasswordValidationCallback.Request request) throws PasswordValidationCallback.PasswordValidationException { final PasswordValidationCallback.PlainTextPasswordRequest plainTextPasswordRequest = (PasswordValidationCallback.PlainTextPasswordRequest) request; final String username = plainTextPasswordRequest.getUsername(); final Authentication authentication; final Authentication userPassAuth = new UsernamePasswordAuthenticationToken(username, plainTextPasswordRequest.getPassword()); authentication = ldapAuthenticationManager.authenticate(userPassAuth); return authentication.isAuthenticated(); } } /** * Assert users. * * @throws Exception error */ public void afterPropertiesSet() throws Exception { Assert.notNull(users, "Users is required."); Assert.notNull(this.ldapAuthenticationManager, "A LDAP Authentication manager is required."); } /** * Sets the users to validate against. Property names are usernames, property values are passwords. * * @param users the users */ public void setUsers(final Properties users) { this.users = users; } /** * The the authentication manager. * * @param ldapAuthenticationManager the provider */ public void setLdapAuthenticationManager(final AuthenticationManager ldapAuthenticationManager) { this.ldapAuthenticationManager = ldapAuthenticationManager; } }
The service config:
The configuration for the Endpoint, CallbackHandler and the LDAP Authentication manager.
The Application Context – Server Side:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:sws="http://www.springframework.org/schema/web-services" xmlns:s="http://www.springframework.org/schema/security" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/web-services http://www.springframework.org/schema/web-services/web-services-2.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.0.xsd"> <sws:annotation-driven/> <context:component-scan base-package="javaitzen.spring.ws"/> <sws:dynamic-wsdl id="exampleService" portTypeName="javaitzen.spring.ws.ExampleServiceEndpoint" locationUri="/exampleService/" targetNamespace="http://www.briandupreez.net/exampleService"> <sws:xsd location="classpath:/xsd/Example1Request.xsd"/> <sws:xsd location="classpath:/xsd/Example1Response.xsd"/> <sws:xsd location="classpath:/xsd/Example2Request.xsd"/> <sws:xsd location="classpath:/xsd/Example2Response.xsd"/> </sws:dynamic-wsdl> <sws:interceptors> <bean id="validatingInterceptor" class="org.springframework.ws.soap.server.endpoint.interceptor.PayloadValidatingInterceptor"> <property name="schema" value="classpath:/xsd/Example1Request.xsd"/> <property name="validateRequest" value="true"/> <property name="validateResponse" value="true"/> </bean> <bean id="loggingInterceptor" class="org.springframework.ws.server.endpoint.interceptor.PayloadLoggingInterceptor"/> <bean class="org.springframework.ws.soap.security.xwss.XwsSecurityInterceptor"> <property name="policyConfiguration" value="/WEB-INF/securityPolicy.xml"/> <property name="callbackHandlers"> <list> <ref bean="callbackHandler"/> </list> </property> </bean> </sws:interceptors> <bean id="callbackHandler" class="javaitzen.spring.ws.CustomValidationCallbackHandler"> <property name="ldapAuthenticationManager" ref="authManager" /> </bean> <s:authentication-manager alias="authManager"> <s:ldap-authentication-provider user-search-filter="(uid={0})" user-search-base="ou=users" group-role-attribute="cn" role-prefix="ROLE_"> </s:ldap-authentication-provider> </s:authentication-manager> <!-- Example... (inmemory apache ldap service) --> <s:ldap-server id="contextSource" root="o=example" ldif="classpath:example.ldif"/> <!-- If you want to connect to a real LDAP server it would look more like: <s:ldap-server id="contextSource" url="ldap://localhost:7001/o=example" manager-dn="uid=admin,ou=system" manager-password="secret"> </s:ldap-server>--> <bean id="marshallingPayloadMethodProcessor" class="org.springframework.ws.server.endpoint.adapter.method.MarshallingPayloadMethodProcessor"> <constructor-arg ref="serviceMarshaller"/> <constructor-arg ref="serviceMarshaller"/> </bean> <bean id="defaultMethodEndpointAdapter" class="org.springframework.ws.server.endpoint.adapter.DefaultMethodEndpointAdapter"> <property name="methodArgumentResolvers"> <list> <ref bean="marshallingPayloadMethodProcessor"/> </list> </property> <property name="methodReturnValueHandlers"> <list> <ref bean="marshallingPayloadMethodProcessor"/> </list> </property> </bean> <bean id="serviceMarshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller"> <property name="classesToBeBound"> <list> <value>javaitzen.spring.ws.Example1</value> <value>javaitzen.spring.ws.Example1Response</value> <value>javaitzen.spring.ws.Example2</value> <value>javaitzen.spring.ws.Example2Response</value> </list> </property> <property name="marshallerProperties"> <map> <entry key="jaxb.formatted.output"> <value type="java.lang.Boolean">true</value> </entry> </map> </property> </bean> </beans>
The Security Context – Server Side:
xwss:SecurityConfiguration xmlns:xwss="http://java.sun.com/xml/ns/xwss/config"> <xwss:RequireTimestamp maxClockSkew="60" timestampFreshnessLimit="300"/> <!-- Expect plain text tokens from the client --> <xwss:RequireUsernameToken passwordDigestRequired="false" nonceRequired="false"/> <xwss:Timestamp/> <!-- server side reply token --> <xwss:UsernameToken name="server" password="server1" digestPassword="false" useNonce="false"/> </xwss:SecurityConfiguration>
The Web XML:
Nothing really special here, just the Spring WS MessageDispatcherServlet.
spring-ws org.springframework.ws.transport.http.MessageDispatcherServlet transformWsdlLocationstrue 1 spring-ws /*
The client config:
To test or use the service you’ll need the following:
The Application Context – Client Side Test:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="messageFactory" class="org.springframework.ws.soap.saaj.SaajSoapMessageFactory"/> <bean id="webServiceTemplate" class="org.springframework.ws.client.core.WebServiceTemplate"> <constructor-arg ref="messageFactory"/> <property name="marshaller" ref="serviceMarshaller"/> <property name="unmarshaller" ref="serviceMarshaller"/> <property name="defaultUri" value="http://localhost:7001/example/spring-ws/exampleService"/> <property name="interceptors"> <list> <ref local="xwsSecurityInterceptor"/> </list> </property> </bean> <bean id="xwsSecurityInterceptor" class="org.springframework.ws.soap.security.xwss.XwsSecurityInterceptor"> <property name="policyConfiguration" value="testSecurityPolicy.xml"/> <property name="callbackHandlers"> <list> <ref bean="callbackHandler"/> </list> </property> </bean> <!-- As a client the username and password generated by the server must match with the client! --> <!-- a simple callback handler to configure users and passwords with an in-memory Properties object. --> <bean id="callbackHandler" class="org.springframework.ws.soap.security.xwss.callback.SimplePasswordValidationCallbackHandler"> <property name="users"> <props> <prop key="server">server1</prop> </props> </property> </bean> <bean id="serviceMarshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller"> <property name="classesToBeBound"> <list> <value>javaitzen.spring.ws.Example1</value> <value>javaitzen.spring.ws.Example1Response</value> <value>javaitzen.spring.ws.Example2</value> <value>javaitzen.spring.ws.Example2Response</value> </list> </property> <property name="marshallerProperties"> <map> <entry key="jaxb.formatted.output"> <value type="java.lang.Boolean">true</value> </entry> </map> </property> </bean>
The Security Context – Client Side:
<xwss:SecurityConfiguration xmlns:xwss="http://java.sun.com/xml/ns/xwss/config"> <xwss:RequireTimestamp maxClockSkew="60" timestampFreshnessLimit="300"/> <!-- Expect a plain text reply from the server --> <xwss:RequireUsernameToken passwordDigestRequired="false" nonceRequired="false"/> <xwss:Timestamp/> <!-- Client sending to server --> <xwss:UsernameToken name="example" password="pass" digestPassword="false" useNonce="false"/> </xwss:SecurityConfiguration>
As usual with Java there can be a couple little nuances when it comes to jars and versions so below is part of the pom I used.
The Dependencies:
3.0.6.RELEASE 2.0.2.RELEASE org.apache.directory.server apacheds-all 1.5.5 jar compile org.springframework.ws spring-ws-core ${spring-ws-version} org.springframework spring-webmvc ${spring-version} org.springframework spring-web ${spring-version} org.springframework spring-context ${spring-version} org.springframework spring-core ${spring-version} org.springframework spring-beans ${spring-version} org.springframework spring-oxm ${spring-version} org.springframework.ws spring-ws-security ${spring-ws-version} org.springframework.security spring-security-core ${spring-version} org.springframework.security spring-security-ldap ${spring-version} org.springframework.ldap spring-ldap-core 1.3.0.RELEASE org.apache.ws.security wss4j 1.5.12 com.sun.xml.wss xws-security 3.0 org.apache.ws.commons.schema XmlSchema 1.4.2 </project>
Reference: Spring 3, Spring Web Services 2 & LDAP Security. from our JCG partner Brian Du Preez at the Zen in the art of IT blog.
相关推荐
10. **Web Services Security**:对于SOAP和其他Web服务,Spring Security提供了安全支持,包括WS-Security标准的实现。 在"spring-security-4.0.0.RELEASE"压缩包中的"dist"目录,可能包含了所有用于构建和运行...
Spring Security 2 的参考手册中文版提供了详细的技术指南,帮助开发者理解和使用这个框架。 在手册的`ns-config.html`章节,你会了解到Spring Security的基础配置,包括如何在Spring应用上下文中定义安全相关的...
3. **Filter Chain(过滤器链)**:Spring Security的核心在于其Web安全过滤器链,如`DelegatingFilterProxy`、`FilterSecurityInterceptor`和`ChannelProcessingFilter`等。这些过滤器在HTTP请求进入应用之前进行...
6. **Web Services Security**:对于基于SOAP的Web服务,Spring Security提供了WS-Security的支持,可以保护服务端点免受未经授权的访问。 7. **CSRF Protection**:Spring Security 4.0.3包含内置的跨站请求伪造...
18.2. 在Spring Security里使用LDAP 18.3. 配置LDAP服务器 18.3.1. 使用嵌入测试服务器 18.3.2. 使用绑定认证 18.3.3. 读取授权 18.4. 实现类 18.4.1. LdapAuthenticator实现 18.4.1.1. 常用功能 18.4.1.2. ...
- WS-Security(通过Spring Web Services) - 流授权(通过Spring Web Flow) #### 它与其它工具的良好集成 - **Spring Portfolio**:与Spring框架家族其他组件协同工作; - **AspectJ**:面向切面编程的支持; - ...
Spring Session提供了跨服务器的会话管理,Spring Web Services支持SOAP服务开发,Spring Shell和Spring Roo则提供了命令行工具和自动化项目生成。 总的来说,Spring Boot和Spring Cloud是构建现代微服务架构的强大...
Then, you'll use Spring Security with the LDAP libraries for authenticating users and create a central authentication and authorization server using OAuth 2 protocol. Further, you'll understand how ...
Spring的其他项目如Spring XD、Spring Mobile、Spring for Android、Spring Web Flow、Spring LDAP、Spring Session、Spring Web Services、Spring Shell、Spring Roo和Spring Scala等,分别针对大数据处理、移动...
27.1. The “Spring Web MVC Framework” 27.1.1. Spring MVC Auto-configuration 27.1.2. HttpMessageConverters 27.1.3. Custom JSON Serializers and Deserializers 27.1.4. MessageCodesResolver 27.1.5. Static...
Spring Web Services则专注于SOAP服务的开发。 Spring Shell和Spring Roo提供命令行工具,前者用于创建命令,后者用于快速生成Spring应用项目。Spring Scala是为Scala语言定制的Spring框架接口,而Spring BlazeDS ...
2. Spring on the web Chapter 5. Building Spring web applications 5.1. Getting started with Spring MVC 5.1.1. Following the life of a request 5.1.2. Setting up Spring MVC 5.1.3. Introducing the Spittr ...
Spring Web Flow关注Web应用的页面流程管理,Spring LDAP简化了对LDAP目录服务的访问。Spring Session提供了跨集群的会话管理,Spring Web Services专注于SOAP服务的开发。Spring Shell和Spring Roo则提供了命令行...
- **Spring Web Services**:简化 SOAP 和 RESTful Web 服务的开发。 - **Spring Security (Acegi Security)**:提供了安全性的支持。 - **Spring LDAP**:简化了与 LDAP 的交互。 - **Spring Rich Client**:...
Spring Web Flow关注Web应用的页面流程管理,Spring LDAP则简化了LDAP目录服务的访问,Spring Session提供了跨服务器的会话管理。Spring Web Services支持SOAP Web服务的创建,Spring Shell和Spring Roo提供了命令行...
- **Spring Web Services**:用于构建基于SOAP和RESTful的Web服务。 - **Spring Modules**:一系列增强Spring功能的模块,例如缓存支持。 - **Spring Security (AcegiSecurity)**:提供认证和授权的安全框架。 - **...
Spring Web Services Spring LDAP Spring Session Spring项目快速搭建 Maven简介 Maven安装 Maven的pom.xml dependencies dependency 变量定义 编译插件 Spring项目的...