Liferay 7 - SOAP Client with Authentication
Apache CXF to the rescue
Soap Client with Authentication in Liferay 7.3
Spricht man heute über Schnittstellen, so ist man schnell bei
REST, GraphQL oder gRPC. Dennoch gibt es einige Gründe sich mit SOAP
auseinanderzusetzen, insbesondere wenn man wie wir im Enterprise
Umfeld unterwegs ist. Einen imho guten Vergleich liefert etwa https://raygun.com/blog/soap-vs-rest-vs-json/ doch
das ist vermutlich nicht der Grund warum Du hier gelandest bist,
sondern die Frage:
Wie zum Teufel bekomme ich einen SOAP Client mit Authentifizierung in Liferay implementiert ???
Der Beantwortung dieser Frage wollen wir uns nun im Folgenden widmen.
Vorhandene Dokumentation und Beispiele
Sucht man nach entsprechender Doku findet man verhältnismäßig wenig. Die Liferay Dokumentation liefert hauptsächlich Ansätze, um Soap Services zur Verfügung zu stellen. Geht es doch um die Client-Seite, dann darum, wie sich Clients gegen die Liferay SOAP API verbinden können.
Sucht man fleißig genug findet sich das Repository von Antonio Mussara https://github.com/amusarra/liferay-72-soap-client-examples der nicht nur die Service Seite, sondern auch die Client Seite zur Verfügung stellt, inklusive einem Ansatz zur SSL/TLS Mutual Authentication. Eine ausführliche Beschreibung findet sich dann noch einmal hier: https://www.dontesta.it/2019/09/05/how-to-implement-a-soap-client-using-jax-ws-liferay-infrastructure/
Damit könnte dieser Blogbeitrag zu Ende sein, wenn wir in freier Wildbahn nicht SOAP Services vorfinden würden, die per Basic Authenticat/UsernameToken/..., kurz: Benutzername und Passwort, gesichert sind. Zu erkennen sind die in Eurer WSDL durch so ein Konstrukt (oder ähnliches):
<?xml version='1.0' encoding='UTF-8'?><definitions xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" xmlns:wspp="http://java.sun.com/xml/ns/wsit/policy" xmlns:wsp="http://www.w3.org/ns/ws-policy" xmlns:wsam="http://www.w3.org/2007/05/addressing/metadata" xmlns:tcp="http://java.sun.com/xml/ns/wsit/2006/09/policy/soaptcp/service" xmlns:sp="http://docs.oasis-open.org/ws-sx/ws-securitypolicy/200702" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:sc="http://schemas.sun.com/2006/03/wss/server" xmlns:occ="common.de.soluvia.opencat.middleware" xmlns:oc="users.de.soluvia.opencat.middleware" xmlns:ns="http://schemas.xmlsoap.org/soap/encoding/" xmlns:fi="http://java.sun.com/xml/ns/wsit/2006/09/policy/fastinfoset/service" xmlns="http://schemas.xmlsoap.org/wsdl/" name="UserSOAPServices" targetNamespace="loremIpsumNamespace"> ... <wsp:Policy wsu:Id="UserServicesPortPolicy"> <wsp:ExactlyOne> <wsp:All> <sp:SupportingTokens> <wsp:Policy> <sp:UsernameToken sp:IncludeToken="http://docs.oasis-open.org/ws-sx/ws-securitypolicy/200702/IncludeToken/AlwaysToRecipient"> <wsp:Policy> <sp:WssUsernameToken10/> </wsp:Policy> </sp:UsernameToken> </wsp:Policy> </sp:SupportingTokens> </wsp:All> </wsp:ExactlyOne> </wsp:Policy>
In SOAP UI ist das ganz einfach: Basic Authentication einrichten, WSS-Password Type einstellen, fertig. Im Zusammenspiel mit Liferay braucht es etwas mehr Aufwand...
In Liferay 6.2 haben wir ganz gute Erfahrungen mit der JAX-WS
Referenzimplentierung Metro gemacht. Für
Liferay 7.X und mit ganzen Umstellung auf OSGI funktionierte das aber
für uns nicht mehr. Zudem bringt Liferay eigentlich alle Libraries auf
Basis von Apache CXF mit... eigentlich. Ohne die Authentifizierung
findet man auch hier, mit genügend Suchenergie, genügend Beispiele wie
der Client zu realisieren ist, ohne auch nur eine CXF Klasse direkt
benutzt zu haben (so wie es sein sollte, denn man möchte sich ja
eigentlich nicht von einem Framework abhängig machen). Die
cxf-rt-ws-security
Erweiterung ist leider nicht dabei
(Quelle: https://github.com/liferay/liferay-portal/blob/master/lib/versions-complete.xml)
Einmal Dependency-Hölle und zurück
Und da fing die Herausforderung an, denn die
cxf-rt-ws-security
Erweiterung hat natürlich ganz viele
Dependencies innerhalb des Apache CXF Systems. Viele sind in Liferay
zwar vorhanden aber nicht direkt zugreifbar, so dass wir jeweils
ClassNotFound Exceptions bekamen. Somit gab es zwei Möglichkeiten:
Entweder alle Dependencies mit compileInclude
in unser
Client Artefakt ziehen (#badidea) oder dafür sorgen, dass alles über
OSGI Module zur Verfügung gestellt wird. Ein erster Ansatz kam hier
wieder von dem bereits oben genannten Antonio Mussara in einem
Blogbeitrag aus 2016 (https://www.dontesta.it/en/2016/07/19/liferay-7-come-realizzare-un-client-soap-apache-cxf-osgi-style/)
Aber auch hier fehlte die Security Erweiterung. Also gab es für uns nur den harten Weg sich durch die Dependency Hölle zu kämpfen. Modul installieren, gucken wo fehlende Abhängigkeiten sind welche nicht-optional sind und das dann über die GogoShell installieren. Dabei stellt man aber fest, dass manche Abhängigkeiten nicht als OSGI-fähige Version vorliegen. Insbesondere die SAML Bibliotheken auf die man zwangsläufig dann stößt, gibt es nicht "einfach so". Hier schafften Bundles aus dem Apache Service Mix Abhilfe. Am Ende erhielten wir folgende Liste an zu installierenden Dependencies:
XmlSchema Core (2.2.1)
Apache CXF Core (3.2.14)
Apache
CXF Runtime JAXB DataBinding (3.2.14)
Apache CXF Runtime SOAP
Binding (3.2.14)
Apache Commons Codec (1.15.0)
Apache CXF
Runtime Simple Frontend (3.2.14)
Apache CXF Runtime JAX-WS
Frontend (3.2.14)
Apache CXF Runtime HTTP Transport
(3.2.14)
Apache CXF Runtime WS Security (3.2.14)
Apache
XML Security for Java (2.2.0)
Apache WSS4J DOM WS-Security
(2.2.5)
Apache WSS4J WS-Security Common (2.2.5)
Apache
WSS4J Streaming WS-Security (2.2.5)
Apache WSS4J WS-Security
Bindings (2.2.5)
Apache CXF Runtime WS Policy (3.2.14)
Apache WSS4J Streaming WS-SecurityPolicy (2.2.5)
Guava: Google
Core Libraries for Java (20.0.0)
Apache ServiceMix :: Bundles ::
opensaml (3.3.0.2)
Apache ServiceMix :: Bundles :: jasypt
(1.9.3.1)
Joda-Time (2.10.8)
Metrics Core (3.2.6)
Apache Neethi (3.1.1)
Apache WSS4J WS-SecurityPolicy model
(2.2.5)
Apache CXF Runtime SAML Security functionality
(3.2.14)
Apache CXF Runtime Security functionality
(3.2.14)
Apache CXF Advanced Logging Feature (3.2.14)
Apache CXF Runtime WS Addressing (3.2.14)
Bei den Versionen haben wir uns zum einen von den von Liferay verwendeten Versionen leiten lassen (Liferay wird seinen Grund dafür haben, z.B. nicht Apache CXF 3.3.X genommen zu haben) und ansonsten die jeweils höchsten, kompatiblen Versionsnummern verwendet.
Serviceorientiert wie wir sind liefern wir natürlich auf direkt die URLs zum Kopieren ;-)
install https://repository.apache.org/content/repositories/releases/org/apache/ws/xmlschema/xmlschema-core/2.2.1/xmlschema-core-2.2.1.jar install https://repository.apache.org/content/repositories/releases/org/apache/cxf/cxf-core/3.2.14/cxf-core-3.2.14.jar install https://repository.apache.org/content/repositories/releases/org/apache/cxf/cxf-rt-databinding-jaxb/3.2.14/cxf-rt-databinding-jaxb-3.2.14.jar install https://repository.apache.org/content/repositories/releases/org/apache/cxf/cxf-rt-bindings-soap/3.2.14/cxf-rt-bindings-soap-3.2.14.jar install https://repo1.maven.org/maven2/commons-codec/commons-codec/1.15/commons-codec-1.15.jar install https://repository.apache.org/content/repositories/releases/org/apache/cxf/cxf-rt-frontend-simple/3.2.14/cxf-rt-frontend-simple-3.2.14.jar install https://repository.apache.org/content/repositories/releases/org/apache/cxf/cxf-rt-frontend-jaxws/3.2.14/cxf-rt-frontend-jaxws-3.2.14.jar install https://repository.apache.org/content/repositories/releases/org/apache/cxf/cxf-rt-transports-http/3.2.14/cxf-rt-transports-http-3.2.14.jar install https://repository.apache.org/content/repositories/releases/org/apache/cxf/cxf-rt-ws-security/3.2.14/cxf-rt-ws-security-3.2.14.jar install https://repo1.maven.org/maven2/org/apache/santuario/xmlsec/2.2.0/xmlsec-2.2.0.jar install https://repo1.maven.org/maven2/org/apache/wss4j/wss4j-ws-security-dom/2.2.5/wss4j-ws-security-dom-2.2.5.jar install https://repo1.maven.org/maven2/org/apache/wss4j/wss4j-ws-security-common/2.2.5/wss4j-ws-security-common-2.2.5.jar install https://repo1.maven.org/maven2/org/apache/wss4j/wss4j-ws-security-stax/2.2.5/wss4j-ws-security-stax-2.2.5.jar install https://repo1.maven.org/maven2/org/apache/wss4j/wss4j-bindings/2.2.5/wss4j-bindings-2.2.5.jar install https://repo1.maven.org/maven2/org/apache/cxf/cxf-rt-ws-policy/3.2.14/cxf-rt-ws-policy-3.2.14.jar install https://repo1.maven.org/maven2/org/apache/wss4j/wss4j-ws-security-policy-stax/2.2.5/wss4j-ws-security-policy-stax-2.2.5.jar install https://repo1.maven.org/maven2/com/google/guava/guava/20.0/guava-20.0.jar install https://repo1.maven.org/maven2/org/apache/servicemix/bundles/org.apache.servicemix.bundles.opensaml/3.3.0_2/org.apache.servicemix.bundles.opensaml-3.3.0_2.jar install https://repo1.maven.org/maven2/org/apache/servicemix/bundles/org.apache.servicemix.bundles.jasypt/1.9.3_1/org.apache.servicemix.bundles.jasypt-1.9.3_1.jar install https://repo1.maven.org/maven2/joda-time/joda-time/2.10.8/joda-time-2.10.8.jar install https://repo1.maven.org/maven2/io/dropwizard/metrics/metrics-core/3.2.6/metrics-core-3.2.6.jar install https://repo1.maven.org/maven2/org/apache/neethi/neethi/3.1.1/neethi-3.1.1.jar install https://repo1.maven.org/maven2/org/apache/wss4j/wss4j-policy/2.2.5/wss4j-policy-2.2.5.jar install https://repo1.maven.org/maven2/org/apache/cxf/cxf-rt-security-saml/3.2.14/cxf-rt-security-saml-3.2.14.jar install https://repo1.maven.org/maven2/org/apache/cxf/cxf-rt-security/3.2.14/cxf-rt-security-3.2.14.jar install https://repo1.maven.org/maven2/org/apache/cxf/cxf-rt-features-logging/3.2.14/cxf-rt-features-logging-3.2.14.jar install https://repo1.maven.org/maven2/org/apache/cxf/cxf-rt-ws-addr/3.2.14/cxf-rt-ws-addr-3.2.14.jar
Wenn Ihr alles richtig gemacht habt, solltet Ihr in Eurer Gogo Shell
in etwa folgendes Bild sehen:
Ggf. müsst Ihr die Module über start <bundleID>
starten.
Nun galt es die build.gradle
anzupassen, damit wir die
neu gewonnenen Funktionen auch nutzen können. Alle relevanten
Abhängigkeiten sind nun compileOnly
und unser Client wird
angenehm klein.
apply plugin: "java" apply plugin: "maven" apply plugin: "cz.swsamuraj.jaxws" group = 'de.ffit.liferay.soap' version = '1.17-SNAPSHOT' buildscript { repositories { mavenLocal() maven { url "https://plugins.gradle.org/m2/" } maven { url "https://repository-cdn.liferay.com/nexus/content/groups/public" } } dependencies { classpath group: "gradle.plugin.cz.swsamuraj", name: "gradle-jaxws-plugin", version: "0.6.1" classpath group: "com.liferay", name: "com.liferay.gradle.plugins", version: "3.2.29" } } repositories { mavenLocal() } dependencies { compileOnly group: 'com.liferay.portal', name: 'release.portal.api', version: '7.3.5-ga6' compileOnly 'org.apache.cxf:cxf-rt-ws-policy:3.2.14' compileOnly 'org.apache.cxf:cxf-rt-transports-http:3.2.14' compileOnly 'org.apache.cxf:cxf-rt-frontend-jaxws:3.2.14' compileOnly 'org.apache.cxf:cxf-core:3.2.14' compileOnly 'org.apache.cxf:cxf-rt-ws-security:3.2.14' testCompile 'org.mockito:mockito-core:2.23.0' testCompile 'junit:junit:4.12' } jaxws { wsdlDir = 'src/wsdl/localhost_8084/middleware' generatedSources = 'src/generated/java' }
Während des Buildprozesses werden nun alle WSDLs die (in unserem
Fall) in /src/wsdl/localhost_8084/middleware
liegen
durchgeparsed und die daraus entstehenden Java Klassen in
src/generated/java
gespeichert.
Der SOAP Client
Für den Client schauen wir uns einmal den UserClient ab, der Nutzerdetails aus einer Middleware holt.
public class UserServiceClient { private static UserServiceClient instance; private UserServices services; private String endpoint = "/UserServices?wsdl"; public static UserServiceClient getInstance() throws Exception { if (instance == null) { instance = new UserServiceClient(); } return instance; } private UserServiceClient() throws Exception { wsdlUrl = generateWsdlURLfromProperties(); } public UserSOAPServices connect() { if (services == null) { services = new UserServices(wsdlUrl); } UserSOAPServices port = services.getUserPort(); attachCXFSecurity(port); reInitializeEndpoint(port); return port; } private URL generateWsdlURLfromProperties(){ middlewareLocation = PrefsPropsUtil.getString("oc.middleware.location"); String portalExtEndpoint = PrefsPropsUtil.getString("ffit.middleware.ws.endpoint.users"); if (portalExtEndpoint != null) { endpoint = portalExtEndpoint; } return new URL(middlewareLocation + endpoint); } private void attachCXFSecurity(Object port){ Client client = ClientProxy.getClient(port); Endpoint endpoint = client.getEndpoint(); Map<String, Object> props = ((BindingProvider) port).getRequestContext(); props.put(WSHandlerConstants.ACTION, WSHandlerConstants.USERNAME_TOKEN); props.put(WSHandlerConstants.PASSWORD_TYPE, WSConstants.PW_TEXT); props.put(WSHandlerConstants.PW_CALLBACK_CLASS, UsernameCallbackHandler.class.getName()); props.put(WSHandlerConstants.USER, "<PUT_YOUR_USERNAME_HERE>"); WSS4JOutInterceptor wssOut = new WSS4JOutInterceptor(props); endpoint.getOutInterceptors().add(wssOut); } private void reInitializeEndpoint(Object port){ ((BindingProvider)port).getRequestContext().put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, wsdlUrl.toString().replace("?wsdl","")); } }
Der "spannende" Teil steckt in der attachCXFSecurity Methode. Hier werden der Benutzername und ein CallbackHandler gesetzt. Der CallbackHandler selbst sieht dann so aus:
import org.apache.wss4j.common.ext.WSPasswordCallback; import org.slf4j.LoggerFactory; import javax.security.auth.callback.*; import java.io.IOException; public class UsernameCallbackHandler implements CallbackHandler { private String USERNAME = "put your username here... better write service"; private String PASSWORD = "your password goes here"; @Override public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { for (Callback callback : callbacks) { if(callback instanceof WSPasswordCallback){ WSPasswordCallback pc = (WSPasswordCallback) callback; pc.setPassword(PASSWORD); }else{ throw new UnsupportedCallbackException(callback, "Unrecognized Callback:"+callback.getClass().getName()); } } } }
Nun sollte alles beisammen sein, damit Ihr Euren mit Benutzername und Passwort geschützten SOAP Webservice aus Liferay 7 heraus aufrufen könnt.
Chris