WCF
WCF permet l'échange de données (lecture et écriture) au travers d'un réseau ou d'un pipe.
WCF introduit le moteur de sérialisation DataContract.
Bon choix pour les communications non-HTTP (MSMQ, Named Pipes) et SOAP.
Pour les services web basés sur HTTP, préférer ASP.NET Core ou ASP.NET Web API 2.x.
Historique
WCF
|
.NET & VS
|
|
4.5 |
4.5 - VS 2012 |
|
4.0 |
4.0 - VS 2010 |
|
3.5 |
3.5 - VS 2008 |
|
3.0 |
3.0 - VS 2005 |
|
Concept
Chaque service expose son contrat via un ou plusieurs endpoints.
Un client WCF se connecte à un service WCF via un endpoint.
endpoint
adresse |
URL
|
binding |
définit le mécanisme de transport des messages
- protocole de communication
- encodage (texte, binaire)
- transport (TCP, HTTP)
|
contract |
|
Binding
|
|
Protocoles de transport
|
Encodage
|
Commentaire
|
BasicHttpBinding |
SOAP 1.1 |
HTTP(S) |
Texte |
Basique
|
WSHttpBinding |
SOAP 1.2 |
HTTP(S) |
Texte |
Complet: fonctionnalités WS-*
|
WsDualHttpBinding |
SOAP 1.2 |
HTTP(S) |
UTF-8 |
Communication bi-directionnelle, les clients et services peuvent envoyer et recevoir des messages
|
NetTcpBinding |
SOAP 1.2 |
TCP |
Binaire |
Communication sur un intranet. Service et client WCF. Plus rapide et fiable.
|
NetNamedPipeBinding |
SOAP 1.2 |
IPC |
Binaire |
Communication entre 2 processus d'une même machine
|
NetMsmqBinding |
SOAP 1.2 |
MSMQ |
Binaire |
Communication déconnectée
|
WebHttpBinding |
REST |
HTTP(S) |
Texte |
|
Type d'hébergement
|
Description
|
Self-Hosting in a Managed Application |
dans une applications console, WPF
|
Managed Windows Services |
dans un service Windows
|
IIS |
HTTP transport seulement
|
Windows Process Activation Service (WAS) |
utilisé par IIS 7.0
|
Liens
Serveur
|
Assembly System.ServiceModel.dll |
IHelloService.cs
|
// Création d'une interface de contrat
[ServiceContract]
public interface IHelloService
{
[OperationContract]
string Hello(Name name);
}
|
HelloService.cs
|
// Implémentation du service
public class HelloService : IHelloService
{
public string Hello(Name name)
{
return $"Hello {name.First} {name.Last}";
}
}
|
Name.cs
|
[DataContract]
public class Name
{
[DataMember]
public string First { get; set; }
[DataMember]
public string First { get; set; }
}
|
App.conf
|
<configuration>
<system.serviceModel>
<!-- Liste des services -->
<services>
<service name="SoapDataService.HelloService">
<!-- Liste des endpoints -->
<endpoint address="net.tcp://localhost:8080/HelloService/"
binding="netTcpBinding"
contract="Contracts.IHelloService" />
<endpoint address="http://localhost/HelloService/"
binding="basicHttpBinding"
contract="Contracts.IHelloService" />
<endpoint address="net.pipe://localhost/HelloService/"
binding="netNamedPipeBinding"
contract="Contracts.IHelloService" />
|
Web Host - IIS - Fichier *.svc
- VS → New Projet → WCF → WCF Service Application
- Ajouter un service: Add → New Item → WCF Service
- Project Properties → Web → Servers → Project Url : http://localhost:port
HelloService.svc
|
<%@ ServiceHost Language="C#" Debug="true" Service="Namespace.HelloService" CodeBehind="HelloService.svc.cs" %>
|
Web.config
|
<system.serviceModel>
<behaviors>
<serviceBehaviors>
<behavior>
<!-- To avoid disclosing metadata information, set the values below to false before deployment -->
<serviceMetadata httpGetEnabled="true" httpsGetEnabled="true"/>
<!-- To receive exception details in faults for debugging purposes, set the value below to true.
Set to false before deployment to avoid disclosing exception information -->
<serviceDebug includeExceptionDetailInFaults="false"/>
</behavior>
</serviceBehaviors>
</behaviors>
<!-- définit les binding par défaut en fonction de scheme -->
<protocolMapping>
<add scheme="http" binding="wsHttpBinding" />
<add scheme="https" binding="wsHttpBinding" />
</protocolMapping>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" />
<!-- WCF configure lui-même les services à partir des fichiers *.svc
il n'est donc pas nécessaire d'ajouter le code suivant -->
<services>
<service name="Namespace.HelloService">
<endpoint address=""
binding="wsHttpBinding"
contract="Namespace.IHelloService" />
</service>
</services>
</system.serviceModel>
|
Web Host - IIS
Project Properties → Web → Servers → Project Url : http://localhost:port
Web.config
|
<system.serviceModel>
<services>
<service name="Service.HelloService">
<!-- adresse est vide ici car elle est définie dans le projet -->
<endpoint address=""
binding="wsHttpBinding"
contract="Contracts.IHelloService" />
</service>
</services>
<!-- fichier svc virtuel -->
<serviceHostingEnvironment>
<serviceActivations>
<add service="Service.HelloService" relativeAddress="HelloService.svc"/>
</serviceActivations>
</serviceHostingEnvironment>
|
Lancement du service par le code - Self Host
|
// assembly System.ServiceModel
ServiceHost host = new ServiceHost(typeof(HelloService));
try
{
// Open the service host to accept incoming calls
host.Open();
// The service can now be accessed.
Console.WriteLine("The service is ready.");
Console.WriteLine("Press <ENTER> to terminate service.");
Console.WriteLine();
Console.ReadLine();
// Close the ServiceHostBase to shutdown the service.
host.Close();
}
catch (CommunicationException commProblem)
{
Console.WriteLine("There was a communication problem. " + commProblem.Message);
Console.Read();
}
|
Client
Le client doit connaître l'interface. Ceci peut être fait par une copie du fichier contenant l'interface dans le projet client ou bien par référence de service.
Génération automatique du proxy
- Lancer le service
- Dans le projet client → clique-droit sur References → Add Service Reference → Coller l'adresse http://domain:port/service.svc
- Changer le namespace: HelloServiceReference
|
// BasicHttpBinding_IHelloService: nom du endpoint du client, à récupérer dans le fichier App.conf du projet client
var proxy = new HelloServiceClient("BasicHttpBinding_IHelloService");
proxy.Endpoint.Address; // http://localhost:53129/HelloService.svc
proxy.Endpoint.Binding; // System.ServiceModel.BasicHttpBinding
proxy.Endpoint.Contract; // System.ServiceModel.Description.ContractDescription
Console.WriteLine(proxy.Hello("everybody !!!"));
|
App.config
|
<!-- code généré par l'ajout de la référence à HelloService -->
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="BasicHttpBinding_IHelloService" />
</basicHttpBinding>
</bindings>
<client>
<endpoint address="http://localhost:56307/HelloService.svc"
binding="basicHttpBinding"
bindingConfiguration="BasicHttpBinding_IHelloService"
contract="HelloServiceReference.IHelloService"
name="BasicHttpBinding_IHelloService" />
</client>
</system.serviceModel>
|
Génération manuelle du proxy
Hardcodé
|
IHelloService proxy = ChannelFactory<IHelloService>.CreateChannel(
new BasicHttpBinding(),
new EndpointAddress("http://localhost:8733/Design_Time_Addresses/SoapDataService/HelloService/"));
IHelloService proxy = ChannelFactory<IHelloService>.CreateChannel(
new NetTcpBinding(),
new EndpointAddress("net.tcp://localhost/HelloService"));
IHelloService proxy = ChannelFactory<IHelloService>.CreateChannel(
new NetNamedPipeBinding(),
new EndpointAddress("net.pipe://localhost/HelloService"));
Console.WriteLine(proxy.Hello("everybody !!!"));
|
Utilisation de la config
App.config
|
<configuration>
<system.serviceModel>
<client>
<!-- s'il y a plusieurs endpoint avec le même contract, il faut leur donner un name pour les différencier. -->
<endpoint address="net.tcp://localhost:8080/HelloService/"
binding="netTcpBinding"
contract="Contracts.IHelloService"
name="tcpEP"/>
<endpoint address="http://localhost/HelloService/"
binding="basicHttpBinding"
contract="Contracts.IHelloService"
name="httpEP"/>
|
|
var factory = new ChannelFactory<IHelloServiceLocal>("httpEP");
IHelloServiceLocal proxy = factory.CreateChannel();
// avec ClientBase
HelloClient proxy = new HelloClient("tcpEP");
proxy.Hello("You!");
// génère un objet proxy à partir de la config
class HelloClient : ClientBase<IHelloService>, IHelloService
{
public HelloClient(string endpointName)
: base(endpointName)
{
}
public string Hello(string name)
{
return Channel.Hello(name);
}
}
|
Configuration
Binding
|
La configuration est à faire sur le serveur et sur les clients. |
Web.config
|
<services>
<service name="Namespace.PersonService">
<endpoint address="http://localhost/PersonService/"
binding="wsHttpBinding"
contract="Namespace.IPersonService"
bindingConfiguration="specificWsHttpBindingConfig"/>
</service>
</services>
<bindings>
<netTcpBinding>
<!-- configuration par défaut pour tous les netTcpBinding -->
<binding></binding>
</netTcpBinding>
<wsHttpBinding>
<!-- configuration nommée -->
<binding name="specificWsHttpBindingConfig"
sendTimeOut="00:01:00"
maxReceivedMessageSize="64000">
<reliableSession inactivityTimeout="00:20:00" order="false"/>
</binding>
</wsHttpBinding>
</bindings>
|
Behavior
|
Seulement sur le serveur. |
Web.config
|
<services>
<service name="Namespace.PersonService" behaviorConfiguration="MyBehavior">
<endpoint address="http://localhost/PersonService/"
binding="wsHttpBinding"
contract="Namespace.IPersonService"/>
</service>
</services>
<behaviors>
<serviceBehaviors>
<!-- sans name, devient le behavior par défaut -->
<behavior name="MyBehavior">
<serviceDebug includeExceptionDetailInFaults="true" />
<serviceThrottling maxConcurrentSessions="100"
maxConcurrentCalls="16"
maxConcurrentInstances="116" />
</behavior>
</serviceBehaviors>
<endpointBehaviors>
</endpointBehaviors>
</behaviors>
|
À la différence de WebAPI, WCF fournit des metadata. Ce qui permet d'avoir une description complète de l'utilisation du service.
Dans VS, l'ajout d'une Service Reference utilise les metadata pour la création automatique de:
- l'interface du contrat
- la classe proxy
- les classe de DataContract
Ceci via l’outil svcutil
Configuration de l'exposition des metadata: http://localhost:8080/mex
Web.config
|
<services>
<service name="Namespace.PersonService">
<host>
<baseAddresses>
<add baseAddress="http://localhost:8080" />
</baseAddresses>
</host>
<endpoint address="net.tcp://localhost:8009/PersonService"
binding="netTcpBinding"
contract="Namespace.IPersonService" />
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior>
<serviceMetadata httpGetEnabled="true" />
</behavior>
</serviceBehaviors>
</behaviors>
|
Web.config
|
<services>
<service name="Namespace.PersonService">
<host>
<baseAddresses>
<add baseAddress="http://localhost:8080" />
</baseAddresses>
</host>
<endpoint address="net.tcp://localhost:8009/PersonService"
binding="netTcpBinding"
contract="Namespace.IPersonService" />
<endpoint address="mex"
binding="mexHttpBinding"
contract="IMetadataExchange" />
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior>
<serviceMetadata />
</behavior>
</serviceBehaviors>
</behaviors>
|
|
PerSession par défaut. |
|
[ServiceBehavior(InstanceContextMode=InstanceContextMode.PerSession)]
public class HelloService : IHelloService
|
InstanceContextMode
|
Description
|
PerCall |
A new InstanceContext is created for each client request.
|
PerSession |
A new InstanceContext is created for each new client session and maintained for the lifetime of that session. This requires a binding that supports transport sessions: TCP, IPC, WS-HTTP with Reliability or Security
|
Single |
A single InstanceContext handles all client requests for the lifetime of the application.
|
Concurrency
|
Single par défaut. |
|
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single,
ConcurrencyMode = ConcurrencyMode.Multiple)]
public class HelloService : IHelloService
|
ConcurrencyMode
|
Description
|
Single |
Un seul appel à la fois par proxy. Les autres appels sont mis en attente. ThreadSafe.
|
Multiple |
Tous les appels sont gérés en même temps. Locker les modifications des données.
|
Reentrant |
Similaire à Single, mais permet plusieurs appels en même temps pour le même client (callback).
|
Contract Mismatch Equivalency
L'interface entre le serveur et le client doit être la même.
Si ce n'est pas le cas, il faut gérer les différences: Namespace, nom de l'interface, noms des méthodes.
Server/IHelloService.cs
|
namespace Server
{
[ServiceContract(Namespace = "http://localhost/HelloService")]
public interface IHelloService
{
[OperationContract]
string Hello(string name);
}
}
|
Client/IHelloServiceClient.cs
|
namespace Client
{
// même Namespace que pour le server
// changement du nom de l'interface
[ServiceContract(Namespace = "http://localhost/HelloService", Name = "IHelloService")]
interface IHelloServiceClient
{
// changement du nom de la méthode
[OperationContract(Name = "Hello")]
string HelloClient(string name);
}
}
|
ContractNamesapce Assembly Attribut
Permet de définir le Namespace de tous les contrats de cette assembly.
La définition du Namespace dans l'atribut ServiceContract peut donc être supprimée.
AssemblyInfo.cs
|
// assembly System.Runtime.Serialization
[assembly: ContractNamespace("http://localhost/HelloService", ClrNamespace = "Server")]
|
ExtensibleDataObject
Si on ajoute une propriété sur une classe de données sur le serveur mais pas sur celle du client, permet de récupérer quand même cette propriété sur le client.
HostConsole/Person.cs
|
namespace HostConsole
{
[DataContract]
class Person
{
[DataMember]
public string Name { get; set; }
[DataMember]
public int Age { get; set; }
}
}
|
ClientConsole/Person.cs
|
namespace ClientConsole
{
[DataContract]
class Person : IExtensibleDataObject
{
[DataMember]
public string Name { get; set; }
public ExtensionDataObject ExtensionData { get; set; }
}
}
|
Faults and Exceptions
Pour transmettre les exceptions CLR du serveur au client, WCF utilise les SOAP Faults.
- Service lance une exception
- WCF package l'exception dans une SOAP fault et l'envoie comme message de réponse
- Client recréé l'exception CLR à partir de la SOAP fault
Unhandled IncludeExceptionDetailInFaults = false |
- Le client reçoit une FaultException
- pas d'informations supplémentaires
- le proxy a un State Faulted et ne peut plus être utilisé
|
Unhandled IncludeExceptionDetailInFaults = true |
- Le client reçoit une FaultException<ExceptionDetail>
- accès au message et à la StackTrace de l'exception
- le proxy a un State Faulted et ne peut plus être utilisé
|
FaultException FaultException<T> |
- Le client reçoit une FaultException<T>
- accès au message et à la StackTrace de l'exception
- le proxy est toujours viable
|
Web.config
|
<behaviors>
<serviceBehaviors>
<behavior>
<serviceDebug includeExceptionDetailInFaults="true" />
|
HelloService.svc.cs
|
[ServiceBehavior(IncludeExceptionDetailInFaults = true)]
public class HelloService : IHelloService
{ ... }
|
FaultException<T>
CustomServerException.cs
|
[DataContract]
public class CustomServerException
{
[DataMember]
public string Message { get; set; }
[DataMember]
public string When { get; set; }
[DataMember]
public string User { get; set; }
}
|
IHelloService.cs
|
[OperationContract]
[FaultContract(typeof(CustomServerException))]
string Hello();
|
HelloService.svc.cs
|
string Hello()
{
var ex = new CustomServerException()
{
Message = "Error message",
When = DateTime.Now,
User = "Nicolas"
};
throw new FaultException<CustomServerException>(ex, "reason");
|
WcfClient.cs
|
catch (FaultException<CustomServerException> ex)
{
CustomServerException csex = ex.Detail;
}
|
|
DataContractSerializer ne permet pas la (dé)sérialisation entre le client et le serveur, car le xml généré contient le xmlns correspondant au namespace des classes qui est différent entre le client et le serveur.
Utiliser DataContractJsonSerializer ou [XmlSerializerFormat()] |
Serveur REST
IPersonService.cs
|
// Création d'une interface de contrat
[ServiceContract]
[XmlSerializerFormat()] // utilisation de XmlSerializer au lieu de DataContractSerializer
public interface IPersonService
{
[OperationContract]
[WebGet(ResponseFormat = WebMessageFormat.Json)] // réponse au format JSON, XML est le format par défaut
IEnumerable<Person> GetAllPersons();
[OperationContract]
[WebGet(UriTemplate = "persons/{id}")]
Person GetPerson(string id);
[OperationContract]
[WebInvoke(Method = "POST", UriTemplate = "persons")]
void AddPerson(Person person);
[OperationContract]
[WebInvoke(Method = "PUT", UriTemplate = "persons/{id}")]
void ModifyPerson(Person person);
[OperationContract]
[WebInvoke(Method = "DELETE", UriTemplate = "persons/{id}")]
void DeletePerson(int id);
}
|
PersonService.cs
|
// Implémentation du service
public class PersonService : IPersonService
{
public IEnumerable<Person> GetAllPersons() {}
public Person GetPerson(string id) {}
public void AddPerson(Person person) {}
public void ModifyPerson(Person person) {}
public void DeletePerson(int id) {}
}
|
Person.cs
|
[DataContract]
public class Person
{
[DataMember]
public string Id { get; set; }
[DataMember]
public string Name { get; set; }
}
|
App.conf
|
<system.serviceModel>
<!-- Liste des services -->
<services>
<service name="RestDataService.PersonService">
<host>
<baseAddresses>
<add baseAddress = "http://localhost:8733/Design_Time_Addresses/RestDataService/PersonService/" />
</baseAddresses>
</host>
<endpoint address=""
binding="webHttpBinding"
contract="RestDataService.IPersonService"
behaviorConfiguration="restfulBehavior" />
<endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>
</service>
</services>
<behaviors>
<endpointBehaviors>
<behavior name="restfulBehavior">
<webHttp />
</behavior>
</endpointBehaviors>
</behaviors>
|
Client REST
Le client doit connaître l'interface. Ceci peut être fait par une copie du fichier contenant l'interface dans le projet client ou bien par référence de service.
|
// récupération de données, envoie d'une requête GET
var uri = "http://localhost:8733/Design_Time_Addresses/RestDataService/PersonService/GetAllPersons";
var request = WebRequest.Create(uri) as HttpWebRequest;
request.KeepAlive = false;
request.Method = "GET";
using (var responseWeb = request.GetResponse() as HttpWebResponse)
{
// JSON
using (var responseStreamReader = new StreamReader(responseWeb.GetResponseStream()))
{
var responseString = responseStreamReader.ReadToEnd();
var allPersons = JsonConvert.DeserializeObject<List<Person>>(responseString);
}
// DataContract
using (var reader = XmlDictionaryReader.CreateTextReader(responseWeb.GetResponseStream(), new XmlDictionaryReaderQuotas()))
{
var dataContractSerializer = new DataContractSerializer(typeof(List<Person>));
var allPersons = dataContractSerializer.ReadObject(reader) as List<Person>;
}
}
|
|
// envoie de données, envoie d'une requête POST
string dataToSend = null;
var serializer = new DataContractJsonSerializer(typeof(Person));
using (var stream = new MemoryStream())
{
serializer.WriteObject(stream, personToAdd);
dataToSend = Encoding.UTF8.GetString(stream.ToArray(), 0, (int)stream.Length);
}
var uri = "http://localhost:8733/Design_Time_Addresses/RestDataService/PersonService/AddPerson";
var webClient = new WebClient();
webClient.Headers["Content-type"] = "application/json";
webClient.Encoding = Encoding.UTF8;
webClient.UploadString(uri, "POST", dataToSend);
|
Serveur REST ODATA
|
Utiliser un WCF Service Application et pas un WCF Service Library. |
PersonService.svc
|
public class PersonService : DataService<DataSourceWrapper>
{
public static void InitializeService(DataServiceConfiguration config)
{
config.SetEntitySetAccessRule("*", EntitySetRights.All);
config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V3;
}
}
|
DataSourceWrapper.cs
|
public class DataSourceWrapper
{
static IEnumerable<Contact> _persons;
public IQueryable<Contact> Persons
{
get
{
return _persons.AsQueryable();
}
}
static DataSourceWrapper()
{
// chargement des personnes
_persons = ...
}
}
|
Person.cs
|
[DataServiceKey("Id")]
[DataServiceEntity]
public class Person
{
public string Id { get; set; }
public string Name { get; set; }
}
|
Web.config
|
<system.serviceModel>
<services>
<service name="PersonService">
<endpoint address=""
binding="webHttpBinding"
contract="System.Data.Services.IRequestHandler" />
|
Url de test:
http://localhost:port/PersonService.svc/Persons |
Toutes les personnes
|
http://localhost:port/PersonService.svc/Persons('id') |
Sélection d'une personne par son id
|
http://localhost:port/PersonService.svc/Persons('id')/Name |
Sélection de la propriété Name d'une personne
|
http://localhost:port/PersonService.svc/Persons('id')/Name/$value |
Sélection de la valeur de la propriété Name
|
http://localhost:port/PersonService.svc/Persons?$filter=Firstname eq 'Billy' |
filtre: toute les personnes dont le nom est Billy
|
Client REST ODATA
Ajouter une Service Reference pour créer les classes DataSourceWrapper et Person dans le client.
|
Uri uri = new Uri("http://localhost:port/PersonService.svc/");
DataSourceWrapper client = new RestODataServiceReference.DataSourceWrapper(uri);
IEnumerable<Person> persons = client.Persons.Execute();
|
Callback
Méthode cliente appelée par le serveur en retour d'un premier appel.
Serveur
Interface de contrat
|
[ServiceContract()]
public interface IServicePhotoRappel
{
[OperationContract(IsOneWay = true)]
void PhotoTrouvee(Photo photo);
}
[ServiceContract(CallbackContract = typeof(IServicePhotoRappel))]
public interface IServicePhoto
{
[OperationContract]
void RecherchePhoto(string photoId);
}
|
Implémentation du service
|
public class ServicePhoto : IServicePhoto
{
public void RecherchePhoto(string photoId)
{
Console.WriteLine("je recherche ...");
Thread.Sleep(3000);
Console.WriteLine("j'ai trouvé\n");
IServicePhotoRappel rappel =
OperationContext.Current.GetCallbackChannel<IServicePhotoRappel>();
// Appel du callback sur le client
rappel.PhotoTrouvee(new Photo()
{
Titre = "Photo1",
Commentaire = "No comment",
Image = new byte[] { 1, 2 }
});
}
}
|
La classe Photo
|
[DataContract]
public class Photo
{
[DataMember]
public string Titre { get; set; }
[DataMember]
public byte[] Image { get; set; }
[DataMember]
public string Commentaire { get; set; }
}
|
Client
|
class ServicePhotoRappel : IServicePhotoRappel
{
// Callback appelé par le serveur
public void PhotoTrouvee(Photo photo)
{
Console.WriteLine("Photo " + photo.Titre + " reçue");
}
}
static void Main(string[] args)
{
InstanceContext context = new InstanceContext(new ServicePhotoRappel());
IServicePhoto proxy = DuplexChannelFactory<IServicePhoto>.CreateChannel(
new InstanceContext(new ServicePhotoRappel()),
new NetTcpBinding(),
new EndpointAddress("net.tcp://localhost/ServicePhoto"));
Console.WriteLine("Lancement d'une recherche ...");
proxy.RecherchePhoto("id");
Console.WriteLine("Fin de la demande");
}
|
Scénario
|
Binding
|
Authentification
|
Intranet |
TCP Binding |
Windows
|
Internet |
HTTP Binding |
Authentification protégée par un certificat, puis Windows ou ASP.NET
|
Identity
Token / Host |
Identity du processus hôte, celui qui accède aux ressources |
WindowsIdentity.GetCurrent().Name
|
Primary |
Identity du client |
ServiceSecurityContext.Current.PrimaryIdentity.Name
|
Windows |
Dans le cas d'une authentification Windows, même Identity que Primary, sinon vide |
ServiceSecurityContext.Current.WindowsIdentity.Name
|
Thread |
Même que Primary |
Thread.CurrentPrincipal.Identity.Name
|
Intranet Security
- Authentification avec les Windows Credentials
- Les Windows Credentials du client sont envoyés automatiquement au service. Ok pour une Desktop App, mais pose problème pour une WebApp, car le client est IIS.
App.config
|
<services>
<service name="HostConsole.PersonService">
<endpoint address="net.tcp://localhost:8010/Service/"
binding="netTcpBinding"
contract="HostConsole.IService"/>
<endpoint address="net.tcp://localhost:8010/ServiceAdmin/"
binding="netTcpBinding"
contract="HostConsole.IAdminService"
bindingConfiguration="admin"/>
</service>
</services>
<bindings>
<netTcpBinding>
<binding transactionFlow="true" sendTimeout="00:20:00">
<!-- turn off security -->
<security mode="None" />
</binding>
<binding name="admin" transactionFlow="true" sendTimeout="00:20:00">
<security mode="Transport">
<!-- Windows Auth (token) -->
<transport clientCredentialType="Windows" />
</security>
</binding>
</netTcpBinding>
</bindings>
|
|
// changer les credentials client
var factoryAdmin = new ChannelFactory<IAdminService>("tcpAdminEP");
factoryAdmin.Credentials.Windows.ClientCredential.UserName = "";
factoryAdmin.Credentials.Windows.ClientCredential.Domain = "";
factoryAdmin.Credentials.Windows.ClientCredential.Password = "";
IPersonAdminService proxyAdmin = factoryAdmin.CreateChannel();
var proxyAdmin = new AdminClient("tcpAdminEP");
proxyAdmin.ClientCredentials.Windows.ClientCredential.UserName = "";
proxyAdmin.ClientCredentials.Windows.ClientCredential.Domain = "";
proxyAdmin.ClientCredentials.Windows.ClientCredential.Password = "";
|
MyService.cs
|
// restreint l'accès aux utilisateur authentifiés qui appartiennent au role Administrator
// sinon SecurityAccessDeniedException : Access is denied.
[PrincipalPermission(SecurityAction.Demand, Role = "Administrators")]
public void DoSomething()
{ ... }
|
Impernation pour les applications web clientes
|
using (((WindowsIdentity)User.Identity).Impersonate())
{
var factory = new ChannelFactory<IService>("tcpEP");
IService proxy = factory.CreateChannel();
proxy.DoSomething();
proxy.Close();
}
|
Internet Security
- Pour permettre à tous les types de clients (non-Windows), l'authentification de fait avec un Username et un Password.
- On utilise un certificat pour chiffrer les échanges (Username, Password)
- Sur le serveur, l'authentification peut être fait via Windows ou ASP.NET
- Négociation
- PeerTrust le client a une copie du certificat (sans la clé privé)
- ChainTrust le client a la clé publique encodé en base 64
HostConsole/App.config
|
<services>
<service name="HostConsole.PersonService">
<endpoint address="http://localhost/PersonService/"
binding="wsHttpBinding"
contract="HostConsole.IPersonService"/>
<endpoint address="http://localhost/PersonServiceAdmin/"
binding="wsHttpBinding"
contract="HostConsole.IPersonAdminService"
bindingConfiguration="admin"/>
</service>
</services>
<bindings>
<wsHttpBinding>
<binding transactionFlow="true" sendTimeout="00:20:00">
<security mode="None" />
</binding>
<binding name="admin" transactionFlow="true" sendTimeout="00:20:00">
<security mode="Message">
<message clientCredentialType="UserName" negotiateServiceCredential="false"/>
</security>
</binding>
</wsHttpBinding>
</bindings>
<behaviors>
<serviceBehaviors>
<behavior>
<serviceDebug includeExceptionDetailInFaults="true"/>
<serviceCredentials>
<!-- où se trouve le certificat -->
<serviceCertificate storeLocation="LocalMachine"
storeName="Root"
findValue="WcfEndToEnd"
x509FindType="FindBySubjectName" />
<!-- authentifier le username et password -->
<userNameAuthentication userNamePasswordValidationMode="Windows" />
</serviceCredentials>
<serviceAuthorization principalPermissionMode="UseWindowsGroups" />
</behavior>
</serviceBehaviors>
</behaviors>
|
Client/App.config
|
<client>
<endpoint address="http://localhost/PersonService/"
binding="wsHttpBinding"
contract="ClientConsole.IService"/>
<endpoint address="http://localhost/PersonServiceAdmin/"
binding="wsHttpBinding"
contract="ClientConsole.IAdminService"
bindingConfiguration="admin"
behaviorConfiguration="admin">
<identity>
<certificate encodedValue=
"MIIBuzCCAWWgAwIBAgIQgl2l04hiNqBOBIobr3GjLDANBgkqhkiG9w0BAQQFADAW
MRQwEgYDVQQDEwtSb290IEFnZW5jeTAeFw0xNzEwMzExNTA5NTdaFw0zOTEyMzEy
MzU5NTlaMBYxFDASBgNVBAMTC1djZkVuZHRvRW5kMIGfMA0GCSqGSIb3DQEBAQUA
A4GNADCBiQKBgQDbVwZbsfTaTB7sM9d6SmbsEk4vO6LgCBl0hkHvYK3QHi161hFb
4uP/pS8kQ+tT1XISpVMBb9i30tb9ab/iRe5eJrBuSinCgsibx39jlFx/2yRF2rLf
qhtMm+mgpk44VVHpJK1YjlWITyL6GQTIPk7+6mEmn77o48qwSi75ehWOFQIDAQAB
o0swSTBHBgNVHQEEQDA+gBAS5AktBh0dTwCNYSHcFmRjoRgwFjEUMBIGA1UEAxML
Um9vdCBBZ2VuY3mCEAY3bACqAGSKEc+41KpcNfQwDQYJKoZIhvcNAQEEBQADQQAU
OXu6XwWpqLUMLV0J/pjz1k7yQvyJmApKy3nqiNbKFjwZSH3ODRN78gTdf9yTjwJc
HS5LaczJpR67SVowg+Kr" />
</identity>
</endpoint>
</client>
<behaviors>
<endpointBehaviors>
<behavior name="admin">
<clientCredentials>
<serviceCertificate>
<authentication certificateValidationMode="ChainTrust" />
|
Certificat
ChainTrust
|
L'utilisation des metadata mex permettent d'obtenir le certificat à la création de la référence au service. |
|
REM génération d'un fichier de certificat et installation dans Trusted Root Certification Authorities
REM makecert.exe n'est pas fournit pas Windows
makecert.exe -sr LocalMachine -ss Root -pe -sky exchange -n "CN=WcfEndtoEnd" WcfEndToEnd.cer
|
Générer le fichier de la public key: Win → certmgr.msc → Certificates (Local Computer) → Trusted Root Certification Authorities → Certificates
→ clique-droit sur le certificat → All Tasks → Export:
- Format: Base-64
- Ne pas exporter la clé privée
Client/App.config
|
<client>
<endpoint address="http://localhost/PersonServiceAdmin/"
binding="wsHttpBinding"
contract="ClientConsole.IAdminService"
bindingConfiguration="admin"
behaviorConfiguration="admin">
<identity>
<certificate encodedValue=
"MIIBuzCCAWWgAwIBAgIQgl2l04hiNqBOBIobr3GjLDANBgkqhkiG9w0BAQQFADAW
MRQwEgYDVQQDEwtSb290IEFnZW5jeTAeFw0xNzEwMzExNTA5NTdaFw0zOTEyMzEy
MzU5NTlaMBYxFDASBgNVBAMTC1djZkVuZHRvRW5kMIGfMA0GCSqGSIb3DQEBAQUA
A4GNADCBiQKBgQDbVwZbsfTaTB7sM9d6SmbsEk4vO6LgCBl0hkHvYK3QHi161hFb
4uP/pS8kQ+tT1XISpVMBb9i30tb9ab/iRe5eJrBuSinCgsibx39jlFx/2yRF2rLf
qhtMm+mgpk44VVHpJK1YjlWITyL6GQTIPk7+6mEmn77o48qwSi75ehWOFQIDAQAB
o0swSTBHBgNVHQEEQDA+gBAS5AktBh0dTwCNYSHcFmRjoRgwFjEUMBIGA1UEAxML
Um9vdCBBZ2VuY3mCEAY3bACqAGSKEc+41KpcNfQwDQYJKoZIhvcNAQEEBQADQQAU
OXu6XwWpqLUMLV0J/pjz1k7yQvyJmApKy3nqiNbKFjwZSH3ODRN78gTdf9yTjwJc
HS5LaczJpR67SVowg+Kr" />
</identity>
</endpoint>
</client>
<behaviors>
<endpointBehaviors>
<behavior name="admin">
<clientCredentials>
<serviceCertificate>
<authentication certificateValidationMode="ChainTrust" />
</serviceCertificate>
</clientCredentials>
</behavior>
</endpointBehaviors>
</behaviors>
|
PeerTrust
|
REM génération d'un fichier de certificat et installation dans Personal
REM makecert.exe n'est pas fournit pas Windows
makecert.exe -sr LocalMachine -ss My -pe -sky exchange -n "CN=WcfEndtoEnd" WcfEndToEnd.cer
|
Installer le certificat sur le client dans Trusted People.
|
<client>
<endpoint address="http://localhost/PersonServiceAdmin/"
binding="wsHttpBinding"
contract="ClientConsole.IAdminService"
bindingConfiguration="admin"
behaviorConfiguration="admin">
<identity>
<!-- par défaut, cherche un certificat ayant le même nom que le domain (localhost) -->
<dns value="WcfEndToEnd"/>
</identity>
</endpoint>
</client>
<behaviors>
<endpointBehaviors>
<behavior name="admin">
<clientCredentials>
<serviceCertificate>
<authentication certificateValidationMode="PeerTrust" />
</serviceCertificate>
</clientCredentials>
</behavior>
</endpointBehaviors>
</behaviors>
|
ASP.NET Provider / Identity
Permet de coder son propre moyen d'authentification.
Installer Unity.Wcf qui installe Unity et CommonServiceLocator.
WcfServiceFactory.cs
|
public class WcfServiceFactory : UnityServiceHostFactory
{
protected override void ConfigureContainer(IUnityContainer container)
{
// register all your components with the container here
// container
// .RegisterType<IService1, Service1>()
// .RegisterType<DataContext>(new HierarchicalLifetimeManager());
container.RegisterType<IHelloService, HelloService>()
.RegisterType<IDataRepository, DataRepository>();;
}
}
|
HelloService.svc
|
<!-- ancien code -->
<%@ ServiceHost Language="C#" Debug="true" Service="Namespace.HelloService" CodeBehind="HelloService.svc.cs" %>
<!-- nouveau code -->
<%@ ServiceHost Language="C#" Debug="true" Service="Namespace.HelloService" Factory="Namespace.WcfServiceFactory"" %>
|
HelloService.svc.cs
|
public HelloService(IDataRepository data)
{ ... }
|
Lancer le client au lancement du service
- clique-droit sur le projet service → Properties
- Debug → Start Options
- Command line arguments : /client:"client.exe"
- Working Directory : C:\Solution\ProjetClient\bin\Debug\
WCF Test Client
Outils livré avec VS permettant de tester les WS.
C:\Program Files (x86)\Microsoft Visual Studio\2017\Professional\Common7\IDE\WcfTestClient.exe
Création du certificat:
IIS Manager → sélectionner serveur → Server Certificates → Create Self-Signed Certificate
IIS Self-Signed Certificate créé l'erreur The certificate is not valid for the name ...
|
New-SelfSignedCertificate -DnsName "site.domain.ch -CertStoreLocation "cert:\LocalMachine\My"
|
- Séléctionner le site web → Bindings → Add → Type:https, sélectionner le certificat
- SSL Settings → Require SSL
Web.config
|
<services>
<service name="ServiceTest">
<endpoint address=""
binding="basicHttpBinding"
contract="IServiceTest"
bindingConfiguration="secureHttpBinding"/>
<endpoint address="mex"
binding="mexHttpBinding"
contract="IMetadataExchange" />
<bindings>
<basicHttpBinding>
<binding name="secureHttpBinding">
<security mode="Transport">
<transport clientCredentialType="None"/>
|
Erreurs
The caller was not authenticated by the service
IIS n’autorise l'accès qu'aux utilisateurs Windows.
|
// Modifier les credentials du client
var proxy = new PersonServiceClient("WSHttpBinding_IPersonService");
proxy.ChannelFactory.Credentials.Windows.ClientCredential.UserName = "login";
proxy.ChannelFactory.Credentials.Windows.ClientCredential.Password = "password";
|
Changer la configuration d'IIS: IIS Manager → sélectionner le site → Authentication
Dans le fichier .config du serveur, vérifier le contract du endpoint.
Menu → Turn windows features on or off → Internet Information Services → World Wide Web Services → Application Development Features
Cocher tous sauf CGI
The page you are requesting cannot be served because of the extension configuration
Menu → Turn windows features on or off → .NET Framework Advanced Services → WCF Services → HTTP Activation
The Web server is configured to not list the contents of this directory
Web.config
|
<configuration>
<system.webServer>
<!-- autoriser le serveur à lister son contenu -->
<directoryBrowse enabled="true" />
|