Scarica i sorgenti associati all'articolo.
Le SoapExtension sono delle classi che derivano dalla classe base System.Web.Services.Protocols.SoapExtension e che permettono di entrare nel merito delle fasi di serializzazione e deserializzazione dei messaggi SOAP, sia sul client che sul server. Durante l’elaborazione di una richiesta d’invocazione di un Web Service assistiamo infatti alle seguenti fasi:
· BeforeDeserialize: prima che il messaggio SOAP in input sia deserializzato.
· AfterDeserialize: dopo che il messaggio SOAP in input è stato deserializzato nella richiesta.
· BeforeSerialize: prima che la risposta sia serializzata nel messaggio SOAP di output.
· AfterSerialize: dopo che è stato prodotto il messaggio SOAP di output.
In questo articolo voglio esaminare un caso particolare di SoapExtension da usare sia lato server che lato client: un’estensione che, durante le varie fasi di serializzazione/deserializzazione dei messaggi SOAP, fornisca sul client e controlli sul server l’autenticazione degli utenti tramite un Soap Header gestito in modo automatico e trasparente sia per l’implementazione client che server.
In pratica voglio evitare di dover definire un Soap Header manualmente e di doverlo associare a ogni singolo Web Method dei miei servizi in quanto:
· È oneroso decorare tutti i Web Method con gli stessi attributi SoapHeaderAttribute.
· Potrei dimenticarmi di decorare un Web Method, rendendolo immediatamente insicuro.
· Non voglio “sporcare” il codice implementativo dei singoli Web Method con delle istruzioni che sono infrastrutturali (autenticazione) e non applicative (funzionalità specifiche del servizio).
Partiamo da un ripasso dei concetti fondamentali in merito alle SoapExtension. Sono classi che, come ho già detto, derivano da System.Web.Services.Protocols.SoapExtension, che ha la seguente definizione:
public abstract class SoapExtension {
public virtual abstract void Initialize(object initializer);
public virtual abstract object GetInitializer(Type serviceType);
public virtual abstract object GetInitializer(LogicalMethodInfo methodInfo,
SoapExtensionAttribute attribute);
public virtual abstract void ProcessMessage(SoapMessage message);
public virtual Stream ChainStream(Stream stream) {
return stream;
}
}
I due metodi GetInitializer consentono, come dice il nome stesso, di gestire l’inizializzazione della SoapExtension a seconda che sia attivata sulla base di un attributo .NET custom oppure perché configurata nel file .config dell’applicazione.
Questi due metodi sono invocati una sola volta alla prima inizializzazione dell’estensione. Il metodo Initialize, invece, per ogni nuova istanza dell’estensione, ottiene sempre in ingresso ciò che abbiamo eventualmente deciso di restituire dai metodi GetInitializer.
I metodi per noi importanti in questa sede sono ProcessMessage e ChainStream.
ChainStream ci permette di ottenere una “copia di servizio” dello stream di byte che rappresenta il messaggio SOAP in input o in output, per poter fare delle elaborazioni sullo stesso o anche solo per leggerlo e/o salvarlo (come fanno per esempio le SoapExtension di logging e validazione).
ProcessMessage viene richiamato nelle varie fasi di elaborazione del servizio (BeforeDeserialize, AfterDeserialize, BeforeSerialize, AfterSerialize) e sarà la procedura fondamentale dell’estensione.
Partiamo quindi con lo sviluppo dell’estensione di autenticazione. Per prima cosa dobbiamo definire il Soap Header che sarà destinato a contenere le informazioni di autenticazione:
public class UserNameHeader: SoapHeader
{
public string UserName;
public string UserPassword;
}
Lo faremo però in un progetto a parte configurato come Class Library e non all’interno dei singoli progetti che ospitano i Web Service,; tale progetto conterrà la SoapExtension, il Soap Header e, qualora lo desideriamo, un Attribute che ci permetta comunque di associare manualmente l’estensione ai servizi, qualora ci serva di farlo. Il fatto di realizzare una Class Library ci permetterà di riutilizzare l’estensione in tutti i nostri Web Service.
Il codice della SoapExtension dovrà gestire la sua inizializzazione:
using System;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.IO;
using System.Xml;
using System.Web;
using System.Configuration;
namespace DevLeap.Web.Services.SoapExtensions
{
public class SecuritySoapExtension: SoapExtension
{
// Stream di appoggio durante la manipolazione dei dati
private Stream intermediateStream = null;
private Stream originalStream = null;
// Parametro fittizio
private String SQLConnectionString;
public override object GetInitializer(Type serviceType)
{
// Qui arrivo se la SoapExtension è
// associata al servizio nel web.config
return(ConfigurationSettings.AppSettings[ Ã
"SQLConnectionString"]);
}
public override object GetInitializer(
LogicalMethodInfo methodInfo,
SoapExtensionAttribute attribute)
{
// Qui arrivo se la SoapExtension
// è associata al servizio tramite l'apposito attributo
return(((SecuritySoapExtensionAttribute) Ã
attribute).SQLConnectionString);
}
public override void Initialize(object initializer)
{
// Rileggo il valore precedentemente ottenuto
if (initializer != null)
this.SQLConnectionString = initializer.ToString();
}
// Codice omesso per ora ...
}
}
Si vede chiaramente che sono presenti le due versioni in overload del metodo GetInitializer. Entrambe restituiscono un tipo Object, che potrà contenere qualunque informazione possa esserci utile avere in fase di inizializzazione dell’estensione. L’eventuale oggetto di tipo Object restituito dal metodo GetInitializer sarà poi mantenuto in cache dal motore di ASP.NET e fornito in ingresso al metodo Initialize della SoapExtension per ogni sua nuova istanza. In questo caso ho simulato di gestire la stringa di connessione al database SQL Server che contiene gli utenti e le loro password.
Il metodo GetInitializer che riceve in ingresso il paramentro di tipo SoapExtensionAttribute si aspetta di ricevere in realtà l’attributo che dovremo semmai definire ad hoc e che sarà da utilizzare per associare l’estensione ai singoli Web Service. Un esempio di attributo di questo tipo è il seguente:
[AttributeUsage(AttributeTargets.Method)]
public class SecuritySoapExtensionAttribute: SoapExtensionAttribute
{
public String SQLConnectionString;
private Int32 priority = 0;
public override int Priority
{
get {return this.priority;}
set {this.priority = value;}
}
public override Type ExtensionType
{
get {return typeof(SecuritySoapExtension);}
}
}
Il codice è autoesplicativo. Il parametro Priority rappresenta la priorità dell’estensione corrente rispetto alle altre. La proprietà ExtensionType restituisce proprio il tipo della nostra SoapExtension. Un attributo come questo va utilizzato nella definizione di un Web Method nel modo seguente:
using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Web;
using System.Web.Services;
using DevLeap.Web.Services.SoapExtensions;
namespace SoapExtensionSample
{
[WebService(Namespace="http://schemas.devleap.com/wsSample")]
public class wsSample : System.Web.Services.WebService
{
[WebMethod()]
[SecuritySoapExtension(
SQLConnectionString=
"server=localhost;database=pubs;integrated security=SSPI;")]
public String Metodo()
{
if (Context.Items["UserId"] != null)
return(Context.Items["UserId"].ToString());
else
return(String.Empty);
}
}
}
In alternativa, come dicevo, possiamo associare l’estensione a tutti i Web Service di un progetto web, semplicemente configurandola nel file web.config come si vede in questo esempio:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.web>
<webServices>
<soapExtensionTypes>
<add type="DevLeap.Web.Services.SoapExtensions.SecuritySoapExtension,
DevLeap.Web.Services.SoapExtensions.SecuritySoapExtension" />
</soapExtensionTypes>
</webServices>
</system.web>
</configuration>
Questa seconda soluzione è proprio quella che ci interessa, in quanto permette di accendere l’autenticazione solamente inserendo nel web.config qualche riga di codice XML.
Dal momento che le SoapExtension possono essere utilizzate anche dal lato del client, e quella che stiamo costruendo è proprio pensata per girare sia sul client che sul server, possiamo abilitare una SoapExtension agendo sul file .config anche dell’applicazione client (si veda il file app.config dell’applicazione SoapExtensionSampleConsoleClient allegata all’articolo).
Ma veniamo al cuore della SoapExtension: i metodi ChainStream e ProcessMessage.
Nella sua definizione, la classe SecuritySoapExtension presenta la definizione di due membri privati di istanza di tipo System.IO.Stream . Si tratta di due variabili che faranno da contenitore dello Stream originale e dello Stream di servizio corrispondenti al body del messaggio SOAP. Saranno utilizzati e gestiti dal metodo ChainStream, che si occuperà solo di fare una copia (utilizzando il metodo privato Copy) dello Stream:
public override Stream ChainStream(Stream stream)
{
originalStream = stream;
intermediateStream = new MemoryStream();
return intermediateStream;
}
void Copy(Stream from, Stream to)
{
BinaryReader reader = new BinaryReader(from);
BinaryWriter writer = new BinaryWriter(to);
if ( !from.CanSeek )
{
// 64KB buffer
int bufSize = 64*1024;
byte[] buffer = new byte[bufSize];
while( true )
{
int byteCount = from.Read(buffer,0,bufSize);
if ( byteCount > 0 )
writer.Write(buffer,0,byteCount);
if ( byteCount < bufSize )
break;
}
}
else
{
writer.Write(reader.ReadBytes((int)from.Length));
}
writer.Flush();
to.Flush();
}
A questo punto valutiamo il metodo ProcessMessage che dovrà lavorare con lo Stream di servizio per verificare la presenza del Soap Header di autenticazione, dalla parte del server, e per inserirlo prima dell’invio del messaggio, dalla parte del client.
public override void ProcessMessage(SoapMessage message)
{
if (message is SoapServerMessage)
{
// Codice da eseguire sul server
switch (message.Stage)
{
case SoapMessageStage.BeforeSerialize:
break;
case SoapMessageStage.AfterSerialize:
intermediateStream.Position=0;
Copy(intermediateStream,originalStream);
break;
case SoapMessageStage.BeforeDeserialize:
try
{
String UserId =
CheckAuthenticationHeader(
originalStream,
intermediateStream);
if (UserId != null)
{
HttpContext.Current.Items.Add(
"UserId", UserId);
}
else
{
throw new
System.Security.SecurityException(
"Invalid user!");
}
}
catch (Exception ex)
{
throw new
System.Security.SecurityException(
"Authentication error!", ex);
}
break;
case SoapMessageStage.AfterDeserialize:
break;
}
}
else if (message is SoapClientMessage)
{
// Codice da eseguire sul client
switch (message.Stage)
{
case SoapMessageStage.BeforeSerialize:
UserNameHeader authenticationHeader =
new UserNameHeader();
authenticationHeader.UserName =
ConfigurationSettings.AppSettings["UserName"];
authenticationHeader.UserPassword =
ConfigurationSettings.AppSettings["UserPassword"];
message.Headers.Add(authenticationHeader);
break;
case SoapMessageStage.AfterSerialize:
intermediateStream.Position=0;
Copy(intermediateStream,originalStream);
break;
case SoapMessageStage.BeforeDeserialize:
Copy(originalStream,intermediateStream);
intermediateStream.Position=0;
break;
case SoapMessageStage.AfterDeserialize:
break;
}
}
}
Come si vede, la prima operazione da svolgere è stabilire se il parametro di tipo SoapMessage ottenuto in ingresso è un SoapServerMessage o un SoapClientMessage, per decidere se dobbiamo eseguire la logica client o quella server.
Nello specifico sul client dovremo:
· BeforeSerialize: preparare un’istanza del nostro Soap Header e configurarla con delle credenziali. Nell’esempio sono lette dal file .config del client.
· AfterSerialize: copiare sullo Stream originale lo Stream di servizio.
· BeforeDeserialize: copiare sullo Stream di servizio lo Stream originale.
· AfterDeserialize: non dobbiamo fare nulla.
Mentre sul server:
· BeforeSerialize: non dobbiamo fare nulla.
· AfterSerialize: copiare sullo Stream originale lo Stream di servizio.
· BeforeDeserialize: dobbiamo cercare il Soap Header di autenticazione e validarlo (tramite la procedura CheckAuthenticationHeader illustrata successivamente).
· AfterDeserialize: non dobbiamo fare nulla.
La procedura CheckAuthenticationHeader cercherà, tramite regole XPath, la sezione corrisponde al Soap Header per leggerne il contenuto.
// Procedura utilizzata lato server per verificare la presenza o meno del
// token di autenticazione
private String CheckAuthenticationHeader(Stream inStream, Stream outStream)
{
string result = null;
XmlDocument soapMessage = new XmlDocument();
XmlNamespaceManager namespaceManager;
inStream.Position = 0;
soapMessage.Load(inStream);
namespaceManager = new XmlNamespaceManager(soapMessage.NameTable);
namespaceManager.AddNamespace("soap",
"http://schemas.xmlsoap.org/soap/envelope/");
namespaceManager.AddNamespace("devleap",
"http://schemas.devleap.com/wsSample");
XmlNode AuthenticationHeaderNode =
soapMessage.DocumentElement.SelectSingleNode(
"/soap:Envelope/soap:Header/devleap:UserNameHeader",
namespaceManager);
if (AuthenticationHeaderNode != null)
{
XmlElement AuthenticationHeaderElement =
AuthenticationHeaderNode as XmlElement;
result = AuthenticationHeaderElement.ChildNodes[0].InnerText +
"-" + AuthenticationHeaderElement.ChildNodes[1].InnerText;
AuthenticationHeaderElement.ParentNode.RemoveChild(
AuthenticationHeaderElement);
}
soapMessage.Save(outStream);
outStream.Flush();
outStream.Position = 0;
return result;
}
Sul cavo passerà il seguente messaggio SOAP di input:
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<soap:Header>
<UserNameHeader xmlns="http://schemas.devleap.com/wsSample">
<UserName>paolo</UserName>
<UserPassword>password</UserPassword>
</UserNameHeader>
</soap:Header>
<soap:Body>
<Metodo xmlns="http://schemas.devleap.com/wsSample" />
</soap:Body>
</soap:Envelope>
Il cui output in caso di logon riuscito sarà:
<?xml version="1.0" encoding="utf-8"?> <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <soap:Body> <MetodoResponse xmlns="http://schemas.devleap.com/wsSample"> <MetodoResult>paolo-password</MetodoResult> </MetodoResponse> </soap:Body> </soap:Envelope>

Figura 1 - Il tracing dei messaggi SOAP di Input e di Output.
La risposta è pari alla concatenazione di UserName e UserPassword soltanto perché, con finalità di test, ho deciso di passare attraverso l’HttpContext corrente le credenziali dell’utente.
In questo modo il gioco è fatto! Un client di un Web Service configurato per utilizzare la nostra SoapExtension che dovesse presentarsi senza fornire le credenziali (cioè senza avere la SoapExtension configurata, almeno per ora) si vedrebbe rispondere:
Unhandled Exception: System.Web.Services.Protocols.SoapException: System.Web.Ser vices.Protocols.SoapException: Server was unable to process request. ---> System .Security.SecurityException: User not authenticated!
Mentre un client che invia la richiesta corretta avrà come risposta:
paolo-password Press any key to continue
Quanto visto in questo articolo ha senso finché sia il client che il server sono “sotto il nostro controllo”, questa soluzione infatti è utilizzabile solo se possiamo configurare sia sul client che sul server la nostra SoapExtension.
Pensiamo cosa accadrebbe nel caso in cui il client del nostro Web Service dovesse essere per esempio sviluppato in ambiente Java o con PHP. Come potremmo dare al client un assembly .NET che in automatico crea e configura il Soap Header?
Ovviamente ciò non sarebbe più possibile.
In uno dei prossimi articoli vedremo quindi come definire delle classi SoapExtension particolari (si chiamano SoapExtensionReflector e SoapReflectionImporter) che ci permetteranno di modificare i file WSDL autoprodotti dal motore di ASP.NET quando invochiamo la URL servizio.asmx?WSDL e di personalizzare la generazione della classe proxy sul client.
