Статьи

Spring 3, Spring Web Services 2 и безопасность LDAP

Этот год начался с хорошей ноты, еще один из тех проектов типа «срок не изменится» / «пропустить всю красную ленту» / «Дикий Запад», в которых я должен разобраться и реализовать некоторые функции, используя некоторые относительно новые библиотеки и технологии для изменений, ну, конечно, Spring 3 не нов, но в Java 5, weblogic 10 (.01), Spring 2.5.6 медленный корпоративный мир — все относительно.

Из-за общих временных ограничений я не включаю в этот пост слишком много «пуха», а лишь мелкую мелочь создания и защиты веб-службы Spring 3, Spring WS 2 с использованием нескольких XSD и защиты LDAP.

Код:

Конечная точка службы: ExampleServiceEndpoint
Это класс, который будет представлен как веб-сервис, используя конфигурацию позже в посте.

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
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();
    }
 
}

Код: CustomValidationCallbackHandler

Это был мой собственный код, который я написал для расширения AbstactCallbackHandler, позволяющего нам использовать LDAP.
Согласно комментариям в CallbackHandler ниже, вероятно, будет хорошей идеей иметь менеджер кэша, что-то вроде Hazelcast или Ehcache для кэширования аутентифицированных пользователей, в зависимости от соображений безопасности / производительности.

Приведенный ниже Digest Validator можно просто использовать непосредственно из библиотеки Sun, я просто хотел посмотреть, как он работает.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
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;
    }
}

Конфигурация сервиса:
Конфигурация для конечной точки, CallbackHandler и менеджера аутентификации LDAP.
Контекст приложения — сторона сервера:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
<?xml version="1.0" encoding="UTF-8"?>
       xmlns:context="http://www.springframework.org/schema/context"
              xmlns:s="http://www.springframework.org/schema/security"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
  
  
    <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>

Контекст безопасности — сторона сервера:

1
2
3
4
5
6
7
8
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>

Веб XML:
Здесь нет ничего особенного, только Spring WS MessageDispatcherServlet.

01
02
03
04
05
06
07
08
09
10
11
   spring-ws
   org.springframework.ws.transport.http.MessageDispatcherServlet
 
   transformWsdlLocationstrue
1
 
 
 
   spring-ws
   /*

Конфиг клиента:
Для тестирования или использования сервиса вам понадобится следующее:
Контекст приложения — тест на стороне клиента:

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
<?xml version="1.0" encoding="UTF-8"?>
  
  
    <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>

Контекст безопасности — сторона клиента:

1
2
3
4
5
6
7
8
<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>

Как обычно, в Java может быть несколько небольших нюансов, когда речь идет о банках и версиях, поэтому ниже приведена часть pom, который я использовал.
Зависимости:

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
        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>

Ссылка: Spring 3, Spring Web Services 2 и Безопасность LDAP. от нашего партнера JCG   Брайан Дю Приз в дзен- блоге об искусстве ИТ .