/* * Copyright 2005-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.ws.soap.security; import java.util.Iterator; import java.util.Locale; import javax.xml.namespace.QName; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.util.Assert; import org.springframework.ws.client.WebServiceClientException; import org.springframework.ws.client.support.interceptor.ClientInterceptor; import org.springframework.ws.context.MessageContext; import org.springframework.ws.server.EndpointExceptionResolver; import org.springframework.ws.soap.SoapBody; import org.springframework.ws.soap.SoapFault; import org.springframework.ws.soap.SoapHeader; import org.springframework.ws.soap.SoapHeaderElement; import org.springframework.ws.soap.SoapMessage; import org.springframework.ws.soap.server.SoapEndpointInterceptor; import org.springframework.ws.soap.soap11.Soap11Body; /** * Interceptor base class for interceptors that handle WS-Security. Can be used on the server side, registered in a * {@link org.springframework.ws.server.endpoint.mapping.AbstractEndpointMapping#setInterceptors(org.springframework.ws.server.EndpointInterceptor[]) * endpoint mapping}; or on the client side, on the {@link org.springframework.ws.client.core.WebServiceTemplate#setInterceptors(ClientInterceptor[]) * web service template}. * * <p>Subclasses of this base class can be configured to secure incoming and secure outgoing messages. By default, both are * on. * * @author Arjen Poutsma * @since 1.0.0 */ public abstract class AbstractWsSecurityInterceptor implements SoapEndpointInterceptor, ClientInterceptor { /** Logger available to subclasses. */ protected final Log logger = LogFactory.getLog(getClass()); protected static final QName WS_SECURITY_NAME = new QName("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security"); private boolean secureResponse = true; private boolean validateRequest = true; private boolean secureRequest = true; private boolean validateResponse = true; private boolean skipValidationIfNoHeaderPresent = false; private EndpointExceptionResolver exceptionResolver; /** Indicates whether server-side incoming request are to be validated. Defaults to {@code true}. */ public void setValidateRequest(boolean validateRequest) { this.validateRequest = validateRequest; } /** Indicates whether server-side outgoing responses are to be secured. Defaults to {@code true}. */ public void setSecureResponse(boolean secureResponse) { this.secureResponse = secureResponse; } /** Indicates whether client-side outgoing requests are to be secured. Defaults to {@code true}. */ public void setSecureRequest(boolean secureRequest) { this.secureRequest = secureRequest; } /** Indicates whether client-side incoming responses are to be validated. Defaults to {@code true}. */ public void setValidateResponse(boolean validateResponse) { this.validateResponse = validateResponse; } /** Provide an {@link EndpointExceptionResolver} for resolving validation exceptions. */ public void setExceptionResolver(EndpointExceptionResolver exceptionResolver) { this.exceptionResolver = exceptionResolver; } /** Allows skipping validation if no security header is present. */ public void setSkipValidationIfNoHeaderPresent( boolean skipValidationIfNoHeaderPresent) { this.skipValidationIfNoHeaderPresent = skipValidationIfNoHeaderPresent; } /* * Server-side */ /** * Validates a server-side incoming request. Delegates to {@link #validateMessage(org.springframework.ws.soap.SoapMessage,org.springframework.ws.context.MessageContext)} * if the {@link #setValidateRequest(boolean) validateRequest} property is {@code true}. * * @param messageContext the message context, containing the request to be validated * @param endpoint chosen endpoint to invoke * @return {@code true} if the request was valid; {@code false} otherwise. * @throws Exception in case of errors * @see #validateMessage(org.springframework.ws.soap.SoapMessage,org.springframework.ws.context.MessageContext) */ @Override public final boolean handleRequest(MessageContext messageContext, Object endpoint) throws Exception { if (validateRequest) { Assert.isInstanceOf(SoapMessage.class, messageContext.getRequest()); if(skipValidationIfNoHeaderPresent && !isSecurityHeaderPresent((SoapMessage) messageContext.getRequest())){ return true; } try { validateMessage((SoapMessage) messageContext.getRequest(), messageContext); return true; } catch (WsSecurityValidationException ex) { return handleValidationException(ex, messageContext); } catch (WsSecurityFaultException ex) { return handleFaultException(ex, messageContext); } } else { return true; } } /** * Secures a server-side outgoing response. Delegates to {@link #secureMessage(org.springframework.ws.soap.SoapMessage,org.springframework.ws.context.MessageContext)} * if the {@link #setSecureResponse(boolean) secureResponse} property is {@code true}. * * @param messageContext the message context, containing the response to be secured * @param endpoint chosen endpoint to invoke * @return {@code true} if the response was secured; {@code false} otherwise. * @throws Exception in case of errors * @see #secureMessage(org.springframework.ws.soap.SoapMessage,org.springframework.ws.context.MessageContext) */ @Override public final boolean handleResponse(MessageContext messageContext, Object endpoint) throws Exception { boolean result = true; try { if (secureResponse) { Assert.isTrue(messageContext.hasResponse(), "MessageContext contains no response"); Assert.isInstanceOf(SoapMessage.class, messageContext.getResponse()); try { secureMessage((SoapMessage) messageContext.getResponse(), messageContext); } catch (WsSecuritySecurementException ex) { result = handleSecurementException(ex, messageContext); } catch (WsSecurityFaultException ex) { result = handleFaultException(ex, messageContext); } } } finally { if (!result) { messageContext.clearResponse(); } } return result; } /** Returns {@code true}, i.e. fault responses are not secured. */ @Override public boolean handleFault(MessageContext messageContext, Object endpoint) throws Exception { return true; } @Override public void afterCompletion(MessageContext messageContext, Object endpoint, Exception ex) { cleanUp(); } @Override public boolean understands(SoapHeaderElement headerElement) { return WS_SECURITY_NAME.equals(headerElement.getName()); } /* * Client-side */ /** * Secures a client-side outgoing request. Delegates to {@link #secureMessage(org.springframework.ws.soap.SoapMessage,org.springframework.ws.context.MessageContext)} * if the {@link #setSecureRequest(boolean) secureRequest} property is {@code true}. * * @param messageContext the message context, containing the request to be secured * @return {@code true} if the response was secured; {@code false} otherwise. * @throws Exception in case of errors * @see #secureMessage(org.springframework.ws.soap.SoapMessage,org.springframework.ws.context.MessageContext) */ @Override public final boolean handleRequest(MessageContext messageContext) throws WebServiceClientException { if (secureRequest) { Assert.isInstanceOf(SoapMessage.class, messageContext.getRequest()); try { secureMessage((SoapMessage) messageContext.getRequest(), messageContext); return true; } catch (WsSecuritySecurementException ex) { return handleSecurementException(ex, messageContext); } catch (WsSecurityFaultException ex) { return handleFaultException(ex, messageContext); } } else { return true; } } /** * Validates a client-side incoming response. Delegates to {@link #validateMessage(org.springframework.ws.soap.SoapMessage,org.springframework.ws.context.MessageContext)} * if the {@link #setValidateResponse(boolean) validateResponse} property is {@code true}. * * @param messageContext the message context, containing the response to be validated * @return {@code true} if the request was valid; {@code false} otherwise. * @throws Exception in case of errors * @see #validateMessage(org.springframework.ws.soap.SoapMessage,org.springframework.ws.context.MessageContext) */ @Override public final boolean handleResponse(MessageContext messageContext) throws WebServiceClientException { if (validateResponse) { Assert.isTrue(messageContext.hasResponse(), "MessageContext contains no response"); Assert.isInstanceOf(SoapMessage.class, messageContext.getResponse()); if(skipValidationIfNoHeaderPresent && !isSecurityHeaderPresent((SoapMessage) messageContext.getRequest())){ return true; } try { validateMessage((SoapMessage) messageContext.getResponse(), messageContext); return true; } catch (WsSecurityValidationException ex) { return handleValidationException(ex, messageContext); } catch (WsSecurityFaultException ex) { return handleFaultException(ex, messageContext); } } else { return true; } } /** Returns {@code true}, i.e. fault responses are not validated. */ @Override public boolean handleFault(MessageContext messageContext) throws WebServiceClientException { return true; } @Override public void afterCompletion(MessageContext messageContext, Exception ex) throws WebServiceClientException { cleanUp(); } /** * Handles an securement exception. Default implementation logs the given exception, and returns * {@code false}. * * @param ex the validation exception * @param messageContext the message context * @return {@code true} to continue processing the message, {@code false} (the default) otherwise */ protected boolean handleSecurementException(WsSecuritySecurementException ex, MessageContext messageContext) { if (logger.isErrorEnabled()) { logger.error("Could not secure response: " + ex.getMessage(), ex); } return false; } /** * Handles an invalid SOAP message. Default implementation logs the given exception, delegates to the set {@link * #setExceptionResolver(EndpointExceptionResolver) exceptionResolver} if any, or creates a SOAP 1.1 Client or SOAP * 1.2 Sender Fault with the exception message as fault string, and returns {@code false}. * * @param ex the validation exception * @param messageContext the message context * @return {@code true} to continue processing the message, {@code false} (the default) otherwise */ protected boolean handleValidationException(WsSecurityValidationException ex, MessageContext messageContext) { if (logger.isWarnEnabled()) { logger.warn("Could not validate request: " + ex.getMessage()); } if (exceptionResolver != null) { exceptionResolver.resolveException(messageContext, null, ex); } else { if (logger.isDebugEnabled()) { logger.debug("No exception resolver present, creating basic soap fault"); } SoapBody response = ((SoapMessage) messageContext.getResponse()).getSoapBody(); response.addClientOrSenderFault(ex.getMessage(), Locale.ENGLISH); } return false; } /** * Handles a fault exception.Default implementation logs the given exception, and creates a SOAP Fault with the * properties of the given exception, and returns {@code false}. * * @param ex the validation exception * @param messageContext the message context * @return {@code true} to continue processing the message, {@code false} (the default) otherwise */ protected boolean handleFaultException(WsSecurityFaultException ex, MessageContext messageContext) { if (logger.isWarnEnabled()) { logger.warn("Could not handle request: " + ex.getMessage()); } SoapBody response = ((SoapMessage) messageContext.getResponse()).getSoapBody(); SoapFault fault; if (response instanceof Soap11Body) { fault = ((Soap11Body) response).addFault(ex.getFaultCode(), ex.getFaultString(), Locale.ENGLISH); } else { fault = response.addClientOrSenderFault(ex.getFaultString(), Locale.ENGLISH); } fault.setFaultActorOrRole(ex.getFaultActor()); return false; } /** * Abstract template method. Subclasses are required to validate the request contained in the given {@link * SoapMessage}, and replace the original request with the validated version. * * @param soapMessage the soap message to validate * @throws WsSecurityValidationException in case of validation errors */ protected abstract void validateMessage(SoapMessage soapMessage, MessageContext messageContext) throws WsSecurityValidationException; /** * Abstract template method. Subclasses are required to secure the response contained in the given {@link * SoapMessage}, and replace the original response with the secured version. * * @param soapMessage the soap message to secure * @throws WsSecuritySecurementException in case of securement errors */ protected abstract void secureMessage(SoapMessage soapMessage, MessageContext messageContext) throws WsSecuritySecurementException; protected abstract void cleanUp(); /** * Iterates over header elements and returns true if WS-Security header is found. */ private boolean isSecurityHeaderPresent(SoapMessage message) { SoapHeader soapHeader = message.getSoapHeader(); if(soapHeader == null){ return false; } Iterator<SoapHeaderElement> elements = soapHeader.examineAllHeaderElements(); while(elements.hasNext()){ SoapHeaderElement e = elements.next(); if(e.getName().equals(WS_SECURITY_NAME)){ return true; } } return false; } }