Web Services


Wstęp

jPALIO zostało zintegrowane z WebServices przy użyciu biblioteki Apache CXF. Protokołem wymiany danych jest Simple Object Access Protocol (SOAP). SOAP wykorzystuje język XML i jest standardem organizacji W3C. Głównym zastosowaniem usług sieciowych (ang. web services) jest tworzenie systemów z architekturą zorientowaną na usługi (ang. Service Oriented Architecture, SOA). W tym podejściu do tworzenia aplikacji logika biznesowa to zestaw serwisów dostępnych zarówno dla aplikacji klienckiej zainstalowanej na komputerze użytkownika końcowego jak i innego systemu stworzonego na konkurencyjnej platformie programistycznej, np. J2EE, .Net, PHP.

Konfiguracja

Za integrację jPALIO z WebServices odpowiedzialny jest serwlet palio.webservices.WebServicesServlet. Należy dodać do pliku web.xml dodatkowe wpisy dotyczące tego serwletu.

<servlet>
<servlet-name>webservices</servlet-name>
<servlet-class>palio.webservices.WebServicesServlet</servlet-class>
</servlet>

<servlet-mapping>
<servlet-name>webservices</servlet-name>
<url-pattern>/webservices/*</url-pattern>
</servlet-mapping>


Każda instancja jPALIO może mieć zdefiniową dowolną liczbę niezależnych usług. Usługi są definiowane w pliku konfiguracyjnym instancji.

Najprostsza konfiguracja WebServices:

<webServices>
<service class="sample.TestServiceImpl"/>
<service class="sample.TestService1Impl"/>
...
</webServices>

Atrybut class to pełna nazwa klasy implementującej usługę.


Obsługiwane parametry:

 

Implementacja usługi

W pierwszej kolejności należy stworzyć interfejs usługi. Zadaniem tego intefejsu jest opisanie usługi (nazwa usługi, które metody mają zostać udostępnione, nazwy argumentów itd.). Opisywanie usługi wykonuje się za pomocą adnotacji. Specyfikację oraz opis dostępnych adnotacji znaleźć można tu.


Przykładowy interfejs usługi:

package sample; 

import javax.jws.WebService;
import javax.jws.WebParam;

@WebService
public interface TestService {

public String getText(@WebParam(name="text") String text);

Uwagi do przykładu:


Kolejnym krokiem jest stworzenie klasy implementującej interfejs usługi. Nazwę tej klasy należy podać w konfiguracji usługi.

Przykładowa implementacja interfejsu usługi:

package sample;

import javax.jws.WebService;

@WebService(serviceName="TestService", endpointInterface="sample.TestService")
public class TestServiceImpl implements TestService {

public String getText(String text) {
return "text: " + text
}

}


Uwagi do przykładu:


Zarówno interfejs jak i klasa go implementująca może być zdefiniowany w obiektach jPALIO typu Groovy lub JAVA. Ważne jest to, że modyfikacja tych obiektów nie wymaga restartu jPALIO. W przypadku modyfikacji tych obiektów, jPALIO automatycznie przeładowuje usługi. Poprawność konfiguracji usługi można sprawdzić poprzez uruchomienie strony o podanym adresie:

http://[host]:[port]/palio/webservices

WSDL dla danej usługi generowany jest automatycznie. Dostępny jest pod adresem:

http://[host]:[port]/palio/webservices/[nazwa instancji]/[nazwa usługi]?wsdl

 

Bezpieczeństwo

Sprawdzanie znacznika czasu wiadomości

Przykładowa konfiguracja usługi sieciowej:

<service class="devel.webservices.TestServiceImpl">
<security>
<!-- sprawdzanie ważności znacznika czasu żądań -->
<in>
<param name="action">Timestamp</param>
<param name="timeToLive">300</param>
</in>
<!-- ustawienie znacznika czasu w odpowiedzi -->
<out>
<param name="action">Timestamp</param>
<param name="timeToLive">300</param>
</out>
</security>
</service>

 

Uwagi do przykładu:

 

Autoryzacja typu użytkownik/hasło

Przy autoryzacji użytkownik/hasło wykorzystywane są mechanizmy bezpieczeństwa jPALIO. W domyślnej konfiguracji użytkownicy usługi to użytkownicy systemu (P_USERS). Dla danej usługi można przypisać przywileje, dla których ma być dostępna.

 

Przykładowa konfiguracja usługi sieciowej:

<service class="devel.webservices.TestServiceImpl">
<security>
<!-- autoryzacja żądań -->
<in>
<param name="action">UsernameToken</param>
<param name="privs">admin basic test</param>
</in>
</security>
</service>


Uwagi do przykładu:

 

Istnieje możliwość skonfigurowania własnego mechanizmu autoryzacji żądań. Aby tego dokonać należy w konfiguracji usługi dodać dodatkowy parametr o nazwie authorizationHandlerClass. Jako wartość należy podać pełną nazwę klasy odpowiedzialną za implementację własnego mechanizmu. Klasa ta musi dziedziczyć po klasie palio.webservices.security.UserAuthorizationHandler. jPALIO posiada wbudowane następujące implementacje tej klasy:

palio.webservices.security.PalioUserAuthorizationHandler Autoryzacja i uwierzytelnianie na podstawie wbudowanych mechanizmów w jPALIO (P_USERS)
palio.webservices.security.ServerAdminAuthorizationHandler Uwierzytelnianie na podstawie loginu i hasła administratora serwera jPALIO.

Domyślną implementacją jest palio.webservices.security.PalioUserAuthorizationHandler

Przykładowa konfiguracja z nadpisanym domyślnym mechanizmem autoryzacji:

 

<service class="devel.webservices.TestServiceImpl">
<security>
<in>
<param name="action">UsernameToken</param>
<param name="authorizationHandlerClass">palio.webservices.security.ServerAdminAuthorizationHandler</param>
</in>
</security>
</service>

 

Szyfrowanie

Przykładowa konfiguracja usługi sieciowej:

<service class="devel.webservices.TestServiceImpl">
<security>
<!-- deszyfrowanie żądań -->
<in>
<param name="action">Encrypt</param>
<!-- Względna ścieżka do pliku z parametrami dotyczącymi deszyfrowania -->
<param name="decryptionPropFile">serviceSecurity.properties</param>
<!-- Hasło do klucza prywatnego -->
<param name="privateKeyPassword">skpass</param>
<!-- Określa które części wiadomości zostały zaszyfrowane -->
<param name="encryptionParts"></param>
</in>
<!-- szyfrowanie odpowiedzi -->
<out>
<param name="action">Encrypt</param>
<!-- Względna ścieżka do pliku z parametrami dotyczącymi szyfrowania -->
<param name="encryptionPropFile">serviceSecurity.properties</param>
<!-- Alias klucza publicznego jakim mają być szyfrowane wiadomości -->
<param name="encryptionUser">clientkey</param>
<!-- Określa które części wiadomości mają zostać zaszyfrowane -->
<param name="encryptionParts"></param>
</out>
</security>
</service>

 

Przykładowy plik serviceSecurity.properties:

org.apache.ws.security.crypto.provider=org.apache.ws.security.components.crypto.Merlin
org.apache.ws.security.crypto.merlin.keystore.type=jks
org.apache.ws.security.crypto.merlin.keystore.password=sspass
org.apache.ws.security.crypto.merlin.file=/home/tests/security/serviceKeystore.jks

 

Szczegółowy opis parametrów znaleźć można tutaj.

 

Podpisywanie

Przykładowa konfiguracja usługi sieciowej:

 

<service class="devel.webservices.TestServiceImpl">
<security>
<!-- weryfikacja podpisu żądań -->
<in>
<param name="action">Signature</param>
<!-- Względna ścieżka do pliku z parametrami dotyczącymi podpisywania -->
<param name="signaturePropFile">serviceSecurity.properties</param>
<!-- Określa które części wiadomości zostały podpisane -->
<param name="signatureParts"></param>
</in>
<!-- podpisywanie odpowiedzi -->
<out>
<param name="action">Signature</param>
<!-- Względna ścieżka do pliku z parametrami dotyczącymi podpisywania -->
<param name="signaturePropFile">serviceSecurity.properties</param>
<!-- Alias klucza, który ma zostać użyty do podpisywania -->
<param name="user">servicekey</param>
<!-- Hasło do klucza prywatnego -->
<param name="privateKeyPassword">skpass</param>
<!-- Określa które części wiadomości mają zostać podpisane -->
<param name="signatureParts"></param>
</out>
</security>
</service>

 

 

Przykładowy plik serviceSecurity.properties:

 

org.apache.ws.security.crypto.provider=org.apache.ws.security.components.crypto.Merlin
org.apache.ws.security.crypto.merlin.keystore.type=jks
org.apache.ws.security.crypto.merlin.keystore.password=sspass
org.apache.ws.security.crypto.merlin.file=/home/tests/security/serviceKeystore.jks

 

 

Szczegółowy opis parametrów znaleźć można tutaj.

 

Łączenie mechanizmów bezpieczeństwa

W bardzo prosty sposób można połączyć mechanizmy bezpieczeństwa. Poniżej przedstawiona została konfiguracja realizująca deszyfrowanie, sprawdzanie podpisu oraz sprawdzanie znacznika czasu żądania oraz szyfrowanie, podpisywanie i umieszczanie znacznika czasu w odpowiedzi.

 

<service class="devel.webservices.TestServiceImpl">
<security>
<in>
<param name="action">Encrypt Signature Timestamp</param>

<param name="decryptionPropFile">serviceSecurity.properties</param>
<param name="privateKeyPassword">skpass</param>

<param name="signaturePropFile">serviceSecurity.properties</param>
</in>
<out>
<param name="action">Encrypt Signature Timestamp</param>

<param name="encryptionPropFile">serviceSecurity.properties</param>
<param name="encryptionUser">clientkey</param>

<param name="signaturePropFile">serviceSecurity.properties</param>
<param name="user">servicekey</param>
<param name="privateKeyPassword">skpass</param>
</out>
</security>
</service>

 

 

Generowanie testowych kluczy

Generowanie kluczy usługi i klienta:

keytool -genkey -alias servicekey -keyalg RSA -sigalg SHA1withRSA -keypass skpass -storepass sspass -keystore serviceKeystore.jks -dname "cn=localhost"
keytool -genkey -alias clientkey -keyalg RSA -sigalg SHA1withRSA -keypass ckpass -storepass cspass -keystore clientKeystore.jks -dname "cn=clientuser"

 

Dodanie klucza publicznego klienta do kluczy zaufanych usługi:

keytool -export -rfc -keystore clientKeystore.jks -storepass cspass -alias ClientKey -file Client.cer
keytool -import -trustcacerts -keystore serviceKeystore.jks -storepass sspass -alias ClientKey -file Client.cer -noprompt

 

Dodanie klucza publicznego usługi do kluczy zaufanych klienta:

keytool -export -rfc -keystore serviceKeystore.jks -storepass sspass -alias ServiceKey -file Service.cer
keytool -import -trustcacerts -keystore clientKeystore.jks -storepass cspass -alias ServiceKey -file Service.cer -noprompt

Przykład: implementacja klienta za pomocą JAX-WS Proxy

Kod w języku Java:

package sample;

import java.io.IOException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.xml.namespace.QName;
import javax.xml.ws.Service;

import org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor;
import org.apache.ws.security.WSConstants;
import org.apache.ws.security.WSPasswordCallback;
import org.apache.ws.security.handler.WSHandlerConstants;


public class TestServiceWsClient {

private static final TestServiceWsClient instance = new TestServiceWsClient();

private TestServiceWsClient() {}

private TestService wsClient;

/**
* Gets an instance of service proxy. In multi-threaded environments a synchronization should be used.
*/
public TestService getWsClient() {
if (wsClient == null) {
URL wsdlURL = new URL("http://localhost:8080/palio/webservices/sample/TestService?wsdl");
QName SERVICE_NAME = new QName("http://jpalio.com", "TestService");
Service service = Service.create(wsdlURL, SERVICE_NAME);
wsClient = service.getPort(TestService.class);

// Below code is required if a service is secured by login/password (WS-Security)
org.apache.cxf.endpoint.Client client = org.apache.cxf.frontend.ClientProxy.getClient(wsClient);
org.apache.cxf.endpoint.Endpoint cxfEndpoint = client.getEndpoint();

Map<String,Object> outProps = new HashMap<String,Object>();
outProps.put(WSHandlerConstants.ACTION, WSHandlerConstants.USERNAME_TOKEN);
// Specify our username
outProps.put(WSHandlerConstants.USER, "ws");
// Password type: plain text
outProps.put(WSHandlerConstants.PASSWORD_TYPE, WSConstants.PW_TEXT);
// Callback used to retrieve password for given user.
outProps.put(WSHandlerConstants.PW_CALLBACK_REF, new CallbackHandler() {
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
WSPasswordCallback pc = (WSPasswordCallback) callbacks[0];
// set the password for our message.
pc.setPassword("ws");
}
});

WSS4JOutInterceptor wssOut = new WSS4JOutInterceptor(outProps);
cxfEndpoint.getOutInterceptors().add(wssOut);
}
return wsClient
}

public static TestServiceWsClient getInstance() {
return instance
}
}

 

Przykład użycia:

TestServiceWsClient.getInstance().getWsClient().getText("Hello world!")

 

Przykład: implementacja klienta przy użyciu Adobe Flex Framework

Przedstawiony niżej przykład to implementacja klienta serwisu wymagającego uwierzytelnienia przez podanie nazwy użytkownika oraz hasła. Dostępna jest jedna metoda getText. Wywołanie metody usługi sieciowej jest wykonywane asynchronicznie.

 

Kod przykładu w języku ActionScript 3:

package
{
	import mx.rpc.events.FaultEvent;
	import mx.rpc.events.ResultEvent;
	import mx.rpc.soap.Operation;
	import mx.rpc.soap.SOAPHeader;
	import mx.rpc.soap.WebService;

	public class SampleService
	{
		private static const QNAME_PREFIX:String="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-";
		private static const QNAME_PASSWORD:String=QNAME_PREFIX + "username-token-profile-1.0#PasswordText";
		private static const QNAME_SECURITY:String=QNAME_PREFIX + "wssecurity-secext-1.0.xsd";
		private static const QNAME_UTILITY:String=QNAME_PREFIX + "wssecurity-utility-1.0.xsd";
		private static const WSSE_SECURITY:QName=new QName(QNAME_SECURITY, "Security");

		protected var service:WebService;

		public function SampleService(username:String, password:String)
		{
			service=new WebService();
			service.loadWSDL("http://localhost:8080/palio/webservices/sample/SampleService?wsdl");

			addServiceHeaders(service, username, password);
		}

		public function getText(resultHandler:Function):void
		{
			addResultHandler(Operation(service.getText), resultHandler);

			service.getText();
		}

		protected function addResultHandler(opr:Operation, resultHandler:Function):void
		{
			if (resultHandler != null)
			{
				opr.addEventListener(ResultEvent.RESULT, resultHandler, false, 0, false);
			}
		}

		private function addServiceHeaders(service:WebService, username:String, password:String):void
		{
			var userToken:String="UsernameToken-" + Math.round(Math.random() * 999999).toString();
			var headerXML:XML=<wsse:Security xmlns:wsse={QNAME_SECURITY}>
					<wsse:UsernameToken wsu:Id={userToken} xmlns:wsu={QNAME_UTILITY}>
						<wsse:Username>{username}</wsse:Username>
						<wsse:Password Type={QNAME_PASSWORD}>{password}</wsse:Password>
					</wsse:UsernameToken>
				</wsse:Security>

			var header:SOAPHeader=new SOAPHeader(WSSE_SECURITY, headerXML);
			service.addHeader(header);
		}
	}
}

Przykładowe użycie:

import mx.rpc.events.ResultEvent;

private var service:SampleService = new SampleService("user", "pass");

private function invokeService():void {
service.getText(resultHandler);
}

private function resultHandler(event:ResultEvent):void {
var result:Object=event.result;
trace(result)
}