Surrogates para DataContracts

Como já é sabido, podemos expor um serviço WCF que utiliza tipos complexos, ou seja, uma classe customizada, contendo as propriedades que a descrevem. Com isso, basta referenciá-la em nosso contrato e, consequentemente, os tipos utilizados serão expostos nos metadados (WSDL) do serviço, permitindo aos clientes criar uma representação desta classe. Para exemplificar, eis a classe com a Interface que descreve o contrato do serviço:

[DataContract]
public class Usuario
{
    [DataMember]
    public string Nome { get; set; }
}

[ServiceContract]
public interface IContrato
{
    [OperationContract]
    Usuario Recuperar(Usuario u);
}

Imagine que por algum motivo, voce cria um novo tipo, chamado OutroUsuario e quer fazer o uso deste no contrato, mudando tanto o tipo do retorno quanto o tipo do parametro u do método Recuperar. Com esta mudança, o WSDL continuará referenciando o tipo antigo Usuario, não “quebrando” as referencias atuais. A idéia é alterar o tipo apenas do lado do servidor, não refletindo do lado do cliente. O código abaixo ilustra o tipo OutroUsuario, que será utilizado pelo serviço ao invés do tipo Usuario:

[DataContract(Name = “Usuario”)]
public class OutroUsuario
{
    public string XNome { get; set; }
    public string Dummy { get; set; }
}

[ServiceContract]
public interface IContrato
{
    [OperationContract]
    OutroUsuario Recuperar(OutroUsuario u);
}

Da forma que o contrato está, novas referencias utilizarão o tipo OutroUsuario e, além disso, os clientes atuais mandarão uma instancia de um tipo não conhecido pelo contrato (Usuario) e, não será possível efetuar a deserialização.

Felizmente o WCF disponibiliza um recurso chamado surrogate (substituto). Com a sua utilização, podemos interceptar e customizar a serialização, deserialização e a projeção dos metadados, podendo inclusive substituir um tipo por outro. Para criar um surrogate, é necessário criar uma classe e implementar a Interface IDataContractSurrogate. Entre os principais métodos, temos: GetDataContractType, GetDeserializedObject e GetObjectToSerialize.

O método GetDataContractType é responsável por mapear um tipo em outro, sendo invocado na serialização, deserialização e na importação e exportação dos metadados do serviço. Já o método GetDeserializedObject é invocado quando acontecer a deserialização de um objeto (cliente para o serviço). E, finalmente, o método GetObjectToSerialize é invocado apenas na serialização do objeto (serviço para o cliente). Abaixo temos a implementação do surrogate para exemplo:

public class SurrogateParaUsuario : IDataContractSurrogate
{
    public Type GetDataContractType(Type type)
    {
        return type == typeof(OutroUsuario) ? typeof(Usuario) : type;
    }

    public object GetDeserializedObject(object obj, Type targetType)
    {
        Usuario u = obj as Usuario;
        if (u != null)
            return new OutroUsuario() { Dummy = “ValorPadrao”, XNome = u.Nome };

        return obj;
    }

    public object GetObjectToSerialize(object obj, Type targetType)
    {
        OutroUsuario o = obj as OutroUsuario;
        if (o != null)
            return new Usuario() { Nome = o.XNome + o.Dummy };

        return obj;
    }

    //outros métodos
}

Quando um cliente invocar a operação Recuperar, ele passará a instancia de uma classe Usuario. Quando a mensagem chegar até o serviço, o método GetDataContractType será executado, devendo retornar um Type que representará o tipo em que o objeto deverá ser convertido que, no nosso caso, será o tipo OutroUsuario. Esse método é executado para cada tipo encontrado na operação, descartando os tipos primitivos (string, int, etc.).

Ao analisar o método GetDeserializedObject vemos que a instancia do parametro obj é do tipo Usuario e, neste momento, fazemos o mapeamento do tipo Usuario para o tipo OutroUsuario, mantendo os valores das propriedades fornecidas pelo cliente. Já no método GetObjectToSerialize fazemos o processo inverso, já que o nosso cliente espera a instancia de um objeto Usuario.

Essa classe por si só não funciona. Existe um behavior chamado DataContractSerializerOperationBehavior que nos permite interagir com o serializador do WCF. Ele por sua vez, fornece uma propriedade chamada DataContractSurrogate que recebe uma instancia de uma classe que implemente a Interface IDataContractSurrogate e deverá ser configurada antes da abertura do host, como mostrado abaixo:

host.AddServiceEndpoint(typeof(IContrato), new WSHttpBinding(), “srv”);

OperationDescription od = host.Description.Endpoints[0].Contract.Operations[0];
od.Behaviors.Find<DataContractSerializerOperationBehavior>().DataContractSurrogate =
    new SurrogateParaUsuario();

host.Open();

WCF – Syndication

Web Syndication é uma forma popularmente conhecida que temos para publicar um conteúdo de um determinado site para outros sites ou pessoas. Esta técnica fornece aos seus consumidores um pequeno sumário ou, às vezes, em sua íntegra, conteúdos que foram recentemente adicionados ao site. A partir da versão 3.5, o WCF disponibiliza uma API para suportar a criação de serviços que expõem o seu conteúdo em um dos formatos conhecidos para o syndication e esta API que será abordada no decorrer deste artigo.

Um dos exemplos mais comuns que conhecemos atualmente que fazem o uso desta técnica são sites de conteúdo bastante dinâmico, como é caso de notícias, fóruns e blogs. Disponibilizar o seu conteúdo através do syndication é uma forma simples que temos de manter nossos leitores atualizados, sempre informando que um novo conteúdo foi adicionado ao site. Os sites que publicam este tipo de conteúdo geralmente disponibilizam um endereço onde o mesmo é listado. Esse endereço também é conhecido como feed, e os interessados podem assiná-lo a partir deste endereço, cadastrando-o em um agregador que nada mais é do que uma aplicação Web ou Windows e que tem a finalidade de gerenciar todos os feeds.

Esse conteúdo é exposto via protocolo HTTP, podendo estar em texto puro ou até mesmo em formato HTML. Atualmente temos dois formatos que regem o syndication, sendo eles: RSS (Really Simple Syndication) e ATOM, ambos baseados em XML. O RSS foi criado antes do ATOM e este, por sua vez, tende a suprir algumas necessidades que o formato RSS deixa a desejar e, justamente por isso, as divergências entre os dois formatos são inevitáveis. A comparação e o detalhamento de cada um dos formatos estão além do escopo deste artigo. Se desejar analisar mais detalhes sobre isso, consulte estes endereços: RSS e ATOM.

Devemos separar as classes que o WCF fornece para a criação do syndication em duas seções, tendo a primeira delas classes para a criação dos metadados e dos itens que irão compor o feed e, isso tudo, de forma totalmente independente do formato; já a segunda seção terá as classes de infraestrutura, específicas ao formato (RSS 2.0 ou ATOM 1.0) e que serão responsáveis pela serialização do feed.

As classes que fazem parte da API do syndication estão contidas no namespace System.ServiceModel.Syndication e este, por sua vez, dentro do assembly System.ServiceModel.Web.dll. As primeiras classes que vamos analisar são as classes responsáveis pela serialização dos objetos que irão compor o feed. A primeira classe que temos que analisar é a SyndicationFeedFormatter. Esta classe é marcada como abstrata, servindo como base para todos os outros formatadores e, a partir dela, nascem suas especializações, como o Rss20FeedFormatter e Atom10FeedFormatter. A primeira delas, Rss20FeedFormatter, é responsável pela serialização do feed no formato RSS 2.0, enquanto a segunda é responsável pela serialização no formato ATOM 1.0. Ainda há a versão genérica para os dois formatos, que deve ser utilizada caso você tenha um feed customizado. A imagem abaixo ilustra essa hierarquia:

Figura 1 – Hierarquia dos formatadores.

A criação de syndication a partir do WCF nos obriga a criar o contrato do serviço seguindo algumas exigências. A primeira delas é que o método que retornará os itens (que podem ser notícias, posts, etc.) deve retornar a instância de uma classe que herde da SyndicationFeedFormatter, ou seja, uma das quatro opções que vimos na imagem acima. Neste caso, se quisermos ter as duas versões e permitir que o usuário escolha o formato, teríamos que ter dois métodos diferentes e, cada um deles, retornar a instância concreta do formatador. Uma alternativa a isso é especificar o tipo do retorno do método para a classe base SyndicationFeedFormatter, e o método passaria a receber um parâmetro que indica o formato desejado e, depois do feed criado, você cria a instância concreta da classe correspondente ao formato escolhido. O código abaixo ilustra a criação do contrato baseando-se nesta segunda opção:

using System;
using System.ServiceModel;
using System.ServiceModel.Web;
using System.ServiceModel.Syndication;

[ServiceContract]
[ServiceKnownType(typeof(Atom10FeedFormatter))]
[ServiceKnownType(typeof(Rss20FeedFormatter))]
public interface IBlog
{
    [OperationContract]
    [WebGet(UriTemplate = "GetPosts?formato={formato}")]
    SyndicationFeedFormatter GetPosts(string formato);
}

Apesar do contrato ter apenas um único método, há algumas características que o mesmo utiliza e que não é uma exclusividade do syndication. A primeira delas é o atributo ServiceKnownTypeAttribute. Esse atributo especifica os possíveis tipos que podem ser retornados para o cliente (para maiores detalhes sobre know types, consulte este artigo). Se a operação já retornasse o tipo concreto, não haveria a necessidade de utilizar este atributo. Já a segunda característica trata-se do atributo WebGetAttribute. Este atributo faz parte de um conjunto de tipos que foram adicionados no WCF (também na versão 3.5) para expor serviços a partir do protocolo HTTP e acessíveis através dos métodos GET, PUT, POST, etc. Se desejar conhecer mais detalhes sobre o padrão REST no WCF, visite este endereço. O syndication não exige a criação do envelope SOAP, e justamente por isso que utiliza um formato mais simples de acesso, como o REST.

Depois de conhecer e saber como criar o serializador de um formato específico, precisamos saber como proceder para construir o feed. Ao contrário do formatador, a construção do feed nada sabe sobre qual formato ele será disponibilizado. A idéia aqui é criar os objetos de forma genérica, deixando o formatador se encarregar de como o feed será disponibilizado.

Uma das classes mais importantes neste processo é a SyndicationFeed. É através dela que iremos construir o feed, desde as informações de metadados até a coleção de itens que o mesmo irá disponibilizar. As informações de metadados consistem em quem são os autores, o título, categorias e a descrição do feed. Além dessas informações, temos ainda propriedades onde podemos definir a data de última atualização, idioma e a URL para possíveis imagens. O código abaixo ilustra como criar e configurar a classe que representará o feed:

using System;
using System.ServiceModel.Syndication;

public class BlogService : IBlog
{
    public SyndicationFeedFormatter GetPosts(string formato)
    {
        SyndicationFeed feed = new SyndicationFeed();
        feed.Title = new TextSyndicationContent("Blog do Israel Aece");
        feed.Authors.Add(
            new SyndicationPerson() 
            { 
                Email = "israel@projetando.net"
                , Name = "Israel Aece" 
            });
        feed.Description = new TextSyndicationContent("Blog Técnico");
        feed.Categories.Add(new SyndicationCategory("WCF"));
        feed.Categories.Add(new SyndicationCategory(".NET"));

        //resto da implementação
    }
}

Analisando o código acima nos deparamos com algumas novas classes que merecem ser abordadas. Seguindo a ordem de aparição, temos a classe TextSyndicationContent que representa qualquer conteúdo a ser exibido para o usuário final (mais detalhes sobre ela abaixo). Logo na sequência temos a classe SyndicationPerson que representa o autor do feed, e finalmente a classe SyndicationCategory que tem a finalidade de representar uma categoria de um feed ou de um item. É também importante notar que um feed pode conter um ou vários autores, bem como uma ou várias categorias e, justamente por isso, que a classe SyndicationFeed disponibiliza as coleções Authors e Categories.

Dando sequência no exemplo, a próxima classe a ser mencionada é a SyndicationItem. Cada instância desta classe representa um item (notícias, post, etc.) dentro do feed, mas a sua serialização será feita de forma diferente, dependendo de qual formatador é utilizado. Entre as principais propriedades temos o título (propriedade Title), um resumo da matéria (propriedade Summary) e o endereço até a mesma (propriedade Links). Além das propriedades, essas informações já podem ser definidas logo no construtor desta classe, conforme é mostrado no código abaixo:

using System;
using System.ServiceModel.Syndication;

public class BlogService : IBlog
{
    public SyndicationFeedFormatter GetPosts(string formato)
    {
        SyndicationFeed feed = new SyndicationFeed();
        feed.Title = new TextSyndicationContent("Blog do Israel Aece");
        feed.Authors.Add(
            new SyndicationPerson() 
            { 
                Email = "israel@projetando.net"
                , Name = "Israel Aece" 
            });
        feed.Description = new TextSyndicationContent("Blog Técnico");
        feed.Categories.Add(new SyndicationCategory("WCF"));
        feed.Categories.Add(new SyndicationCategory(".NET"));

        SyndicationItem item1 =
            new SyndicationItem(
                "Message Queue",
                "Descrição do Artigo de <b>Message Queue</b>",
                new Uri("http://www.projetando.net/Sections/ViewArticle.aspx?ArticleID=92"));

        SyndicationItem item2 =
            new SyndicationItem(
                "Transações",
                "Descrição do Artigo de <b>Transações</b>",
                new Uri("http://www.projetando.net/Sections/ViewArticle.aspx?ArticleID=91"));

        feed.Items = new SyndicationItem[] { item1, item2 };
    }
}

Como exemplo, estamos definindo o conteúdo do feed a partir de itens estáticos mas, em uma aplicação real, isso deveria ser extraído de algum repositório, como por exemplo, o banco de dados. Independentemente da origem dos dados que irão compor o feed, cada item deve ser representado por uma instância da classe SyndicationItem e, depois de criado, adicionado de uma coleção temporária, como o List<SyndicationItem>. Isso é necessário pois a classe SyndicationFeed fornece uma propriedade chamada Items que recebe a coleção de itens do feed e esta, por sua vez, espera por uma coleção que implemente a Interface IEnumerable<SyndicationItem>.

Aqui vale uma pausa para comentarmos sobre as classes que encapsulam o conteúdo de um item. Os conteúdos são peças importantes durante a criação do feed e de seus respectivos itens. Os conteúdos não se referem somente ao texto da matéria, mas também trata-se do título, descrição, etc., e esses conteúdos podem ser um texto simples ou HTML. Para encapsular cada um desses diferentes tipos de conteúdos, foi criada uma classe abstrata chamada SyndicationContent e que serve como base para outras classes que lidam com um tipo de conteúdo específico. Para conhecer as classes atuais que representam um determinado tipo de conteúdo, vamos analisar a imagem abaixo:

Figura 2 – Hierarquia das classes de conteúdo.

A primeira delas, UrlSyndicationContent, representa uma URL para algum outro recurso ou até mesmo o endereço para o conteúdo publicado. Já a XmlSyndicationContent consiste em encapsular um conteúdo XML que não será visível ao usuário, e é geralmente utilizado para serializar algum objeto customizado. Finalmente temos a classe TextSyndicationContent, que representa um conteúdo a ser exibido para o usuário, e é capaz de exibir um conteúdo em texto simples ou códigos HTML. Um de seus construtores recebe uma string que representa o conteúdo e uma das opções do enumerador TextSyndicationContentKind que descreve o tipo do conteúdo. As opções fornecidas por esse enumerador são:

  • PlainText: Indica que o conteúdo deve ser texto puro, sem tags.

  • Html: Suporta tags HTML, mas elas serão codificadas.

  • XHtml: O conteúdo será serializado como XML, e as tags não serão codificadas.

Apesar de omitirmos a criação do TextSyndicationContent durante a criação do item (SyndicationItem), o construtor se encarrega de instanciar esta classe e definir o conteúdo com a string que é informada.

Para completar o código do método GetPosts ainda temos que definir o formatador. De acordo com a regra, temos que analisar o parâmetro formato que é passado para o método e, dependendo do seu valor, instanciar o serializador do RSS ou do ATOM. Ambos formatadores têm uma versão do construtor em que aceita uma instância da classe SyndicationFeed contendo as configurações do feed. Para deixar o código mais legível, um método chamado RecuperarFormatador foi criado para instanciar a classe concreta do serializador, de acordo com o parâmetro que é passado para a operação. O código abaixo ilustra essa condicional que determina o serializador:

private static SyndicationFeedFormatter RecuperarFormatador(
string formato,
SyndicationFeed feed) { if (formato == "rss") return new Rss20FeedFormatter(feed); return new Atom10FeedFormatter(feed); }

Hosting

A maioria dos casos onde se utiliza o recurso de syndication é em sites e, como eles são hospedados no IIS, provavelmente utilizaremos ele para servir como host para o serviço que construímos. Sendo assim, podemos adicionar no projeto do site que estamos construindo um item chamado WCF Service. Esse arquivo de extensão *.svc representa um serviço WCF e será ele que representará a classe do serviço, aquela que deve implementar a Interface que corresponde ao contrato. O Visual Studio .NET 2008 também disponibiliza uma template de projeto chamada WCF Service, onde podemos agrupar todos os serviços mas, se já existe o projeto do Web Site, então nada impede de criar dentro dele apenas o arquivo com extensão *.svc.

Como estamos utilizando recursos REST, então vamos expor o serviço a partir do binding WebHttpBinding para que seja possível acessar o feed a partir da URL, sem a necessidade do protocolo SOAP. Isso permitirá que alguns leitores, como é o caso do próprio Internet Explorer 7.0, interpretem o conteúdo XML no próprio navegador e o mostrem de forma amigável ao usuário que está querendo assiná-lo.

Ao utilizar o modelo baseado no arquivo *.svc, toda a configuração do endpoint deverá ser feita de forma declarativa, ou seja, através do arquivo Web.config. Neste tipo de serviço também não é necessária a criação de endpoints para expor os metadados (WSDL) do serviço, já que não faz sentido o usuário referenciá-lo. Para exemplificar a configuração do serviço, vamos analisar o código abaixo:

<?xml version="1.0"?>
<configuration>
  <system.serviceModel>
    <services>
      <service name="BlogService">
        <endpoint address="" binding="webHttpBinding" contract="IBlog"/>
      </service>
    </services>
  </system.serviceModel>
</configuration>

O atributo name do elemento service obrigatoriamente deverá refletir o nome/tipo da classe que representa o serviço. Já o elemento endpoint fornece os atributos address, binding e contract que são autoexplicativos. Para maiores detalhes sobre hosting de serviços WCF, consulte este artigo.

Ao criar um arquivo do tipo WCF Service (*.svc), por padrão, ele possui uma diretiva chamada @ServiceHost que, entre seus atributos temos: Service e CodeBehind. O primeiro atributo reflete o tipo da classe que representa o serviço; já o atributo CodeBehind especifica o caminho físico até o arquivo do serviço. Isso é bem semelhante ao que acontece com o ASP.NET e os arquivos de code-behind. Além disso, ainda precisamos definir o atributo Factory que, quando omitido (que é o padrão), ele assume o tipo ServiceHostFactory (namespace System.ServiceModel.Activation) que, por sua vez, retornará instâncias da classe ServiceHost. Como estamos trabalhando com REST, então devemos alterar o valor do atributo Factory para System.ServiceModel.Activation.WebServiceHostFactory que retornará instâncias da classe WebServiceHost. Para nosso exemplo, a configuração deve ficar como:

<%@ ServiceHost 
        Language="C#" 
        Debug="true" 
        Service="BlogService" 
        Factory="System.ServiceModel.Activation.WebServiceHostFactory"
        CodeBehind="~/App_Code/BlogService.cs" %>

Se executarmos a aplicação já teremos o conteúdo publicado pelo serviço à nossa disposição. Para acessar e visualizar o seu conteúdo não é necessária a criação de uma aplicação cliente, basta apenas acessar a URL diretamente no navegador que já conseguiremos visualizar o conteúdo. Para o exemplo deste artigo, o endereço é: “http://localhost:50569/Host/BlogService.svc/GetPosts?formato=atom&#8221;, podendo variar o parâmetro formato para “rss”, permitindo o usuário escolher o formato desejado simplesmente trocando o valor do parâmetro. A imagem abaixo ilustra o conteúdo interpretado pelo navegador:

Figura 3 – Conteúdo sendo exibido pelo navegador.

Como se pode perceber, temos que informar no endereço a extensão do arquivo *.svc e, em alguns casos, isso não é muito interessante. Se você desejar remover a extensão do arquivo do endereço do feed, você tem duas opções, dependendo de qual versão do IIS está utilizando. Caso esteja utilizando a versão 7.0, então você pode recorrer ao uso do Url Rewrite Module onde, através de expressões regulares, podemos customizar a URL de acesso ao serviço. Abaixo consta uma regra que podemos utilizar para remover a extensão *.svc do endereço:

<?xml version="1.0"?>
<configuration>
  <!-- outras configurações -->
  <system.webServer>
    <rewrite>
      <rules>
        <rule name="RemoverExtensaoSvc" stopProcessing="true">
          <match url="^BlogService/(.*)$" />
          <action type="Rewrite" url="BlogService.svc/{R:1}" />
        </rule>
      </rules>
    </rewrite>
  </system.webServer>
</configuration>

Com a regra acima aplicada ao diretório virtual, podemos acessar o serviço da seguinte forma: “http://localhost/Feeds/BlogService/GetPosts?formato=rss&#8221;.

Para as versões anteriores do IIS, a saída para remover a extensão *.svc do endereço do serviço é escrever um módulo para manipular o endereço. Para isso, devemos criar uma classe e implementar a Interface IHttpModule, analisando no evento BeginRequest a URL solicitada e, caso seja a solicitação para um arquivo *.svc, devemos sobrescrever a requisição através do método RewritePath da classe HttpContext.

Conclusão: O artigo descreveu as principais classes que são fornecidas pelo WCF para a criação de syndication, exibindo as classes que são utilizadas para compor o feed, bem com as classes necessárias para a serialização do mesmo. Para finalizar, a idéia do syndication a partir do WCF é apenas uma alternativa para a criação dos feeds e não um substituto dos padrões ATOM ou RSS.

Syndication.zip (8.89 kb)

WCF – Segurança

Um dos grandes desafios de um software é a segurança do mesmo. Em qualquer software hoje em dia a segurança não consiste apenas em autenticar um usuário, mas também que direitos ele tem dentro do software. As coisas ficam mais complicadas quando falamos de um ambiente distribuído, tornando o processo de autenticação e autorização um pouco mais complexo e, como se não bastasse, temos que nos preocupar com a proteção das requisições que viajam entre o cliente e o servidor.

A finalidade deste artigo é exibir todas as possibilidades que temos para manipular a segurança em serviços WCF. Algumas técnicas serão comentadas mas não serão esgotadas, pois elas merecem um artigo exclusivo para o seu completo entendimento. Já outras técnicas (as mais comuns) de autenticação, autorização e proteção de mensagens serão abordadas neste artigo, exemplificando a sua utilização e configuração.

Modos de Segurança

Apesar de serviços WCF não fugirem muito do padrão de segurança de aplicações convencionais, há algumas exceções e uma delas é a forma de segurança que será aplicada à mensagem. Essa forma de segurança influencia em como a autenticação será realizada e como a mensagem será protegida durante a sua viagem. É importante dizer que se a transferência da mensagem entre o cliente e o serviço ou entre o serviço e o cliente não fosse protegida, a autenticação e autorização estariam completamente vulneráveis, permitindo vários tipos de ataques.

Assim como várias outras configurações, a segurança também é uma característica do binding, podendo efetuar a configuração de forma declarativa ou imperativa. O WCF fornece cinco formas diferentes para tornar segura a transferência da mensagem. Cada uma dessas formas tem suas particularidades e influenciam em como a mensagem será protegida e como a autenticação será realizada. A tabela abaixo exibe essas cinco formas de segurança, detalhando cada uma delas:

Tipo Descrição
None Como o próprio nome já indica, nenhuma espécie de segurança é fornecida e toda a mensagem será trafegada sem criptografia.
Transport Esta opção informa ao WCF que o transporte (TCP, IPC, MSMQ ou HTTPS) irá garantir a segurança da mensagem, criptografando toda a comunicação e, além disso, fornece integridade, privacidade e autenticação mútua. Um dos pontos negativos deste modo é que a segurança será apenas garantida ponto-a-ponto, ou seja, se houver intermediários entre o cliente e o serviço não teremos a garantia de que a mensagem chegará segura até o destino final.
Message Com esta opção toda a mensagem será criptografada, garantindo assim a autenticação mútua e a proteção da mensagem (confidencialidade e integridade). Ao contrário da segurança baseada no transporte, a segurança a nível de mensagem, garante a segurança end-to-end, independentemente de quantos intermediários houver entre o cliente e o serviço, permintindo inclusive que o serviço seja exposto sob um protocolo não seguro, como é o caso do HTTP. Outro grande benefício que existe é que a segurança é baseada em padrões existentes no mercado, o que garantirá a interoperabilidade. Já um ponto negativo desta opção é o overhead que existe, pois toda e qualquer mensagem será criptografada e assinada.
Both Como o próprio nome diz, esta opção utiliza a segurança a nível de transporte e a nível de mensagem, ou seja, a mensagem será protegida e, além disso, será transferida por um transporte seguro. Apesar de maximizar a segurança, isso pode causar uma grande perda de performance e, atualmente, é somente permitido em protocolos específicos, como é o caso do Message Queue, onde a latência não é sentida.
TransportWithMessageCredential Esta opção é um mix das duas anteriores, ou seja, a autenticação do cliente será fornecida a nível de mensagem enquanto a proteção da mensagem (confidencialidade e integridade) e a autenticação do serviço serão fornecidas pela segurança do transporte.
TransportCredentialOnly Apenas a autenticação mútua é fornecida a nível de transporte, não havendo a proteção da mensagem. Esta opção somente está disponível para o binding basicHttpBinding.

A listagem acima possui todas as opções disponíveis, mas isso não quer dizer que todos os bindings fornecidos pelo WCF suportam todas elas. Todos os bindings oferecem uma propriedade chamada Security e, a partir dela, temos uma segunda propriedade chamada Mode que, como já podemos notar, especificará um dos tipos listados na tabela acima através de um enumerador exclusivo.

Para ter uma visão mais detalhada de quais modos de segurança cada binding suporta e, principalmente, qual é a configuração padrão, a tabela abaixo sumariza essas informações, incluindo o tipo do enumerador utilizado por cada binding para determinar o modo desejado (sendo as opções em negrito a configuração padrão):

Binding None Transport Message Mixed Both Enumerador
BasicHttpBinding Sim Sim Sim Sim Não BasicHttpSecurityMode
WebHttpBinding Sim Sim Não Sim Não WebHttpSecurityMode
WSHttpBinding Sim Sim Sim Não Não SecurityMode
WSDualHttpBinding Sim Não Sim Sim Não WSDualHttpSecurityMode
WSFederationHttpBinding Sim Não Sim Sim Não WSFederationHttpSecurityMode
NetTcpBinding Sim Sim Sim Sim Não SecurityMode
NetPeerTcpBinding Sim Sim Sim Sim Não SecurityMode
NetNamedPipeBinding Sim Sim Não Não Não NetNamedPipeSecurityMode
NetMsmqBinding Sim Sim Sim Não Sim NetMsmqSecurityMode
MsmqIntegrationBinding Sim Sim Não Não Não MsmqIntegrationSecurityMode

Uma vez que conhecemos os modos de segurança que temos e que é o binding quem determinará qual deles poderá ser utilizado, chega o momento de saber como devemos proceder para efetuar essa configuração no serviço. Essa configuração poderá ser realizada de forma imperativa ou declarativa. Os bindings fornecem a possibilidade de configurar a segurança a nível de transporte e de mensagem através de propriedades diferentes e, obviamente, que devemos recorrer a uma delas, dependendo do modo escolhido. Os dois trechos de código a seguir ilustram como fazer para efetuar tal configuração de forma imperativa e, na sequência, de forma declarativa:

using System;
using System.ServiceModel;

WSHttpBinding binding = new WSHttpBinding(SecurityMode.Message);

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <bindings>
      <wsHttpBinding>
        <binding name="srvBinding">
          <security mode="Message">
            <!-- outras configurações -->
          </security>
        </binding>
      </wsHttpBinding>
    </bindings>
  </system.serviceModel>
</configuration>

Quando a segurança está habilitada, há uma classe chamada ServiceSecurityContext que representa o contexto de segurança. Se ela estiver sendo acessada no serviço essa classe representará o cliente, e se estiver sendo acessada no cliente representará o serviço. Essa classe fornece uma propriedade estática chamada Current que retorna o contexto atual. As configurações de segurança de um binding e as informações fornecidas pela classe ServiceSecurityContext não param por aí. Vamos ainda explorar vários outros detalhes durante o decorrer deste artigo.

Autenticação

O processo de autenticação consiste em certificarmos que o cliente é quem ele diz ser e, indo mais além, também temos que certificar o serviço. Para efetuar o processo de autenticação, o cliente precisará informar as suas credenciais para que o serviço consiga autenticá-lo. Para isso, o WCF disponibiliza várias possibilidades, e cada uma delas atenderá um cenário específico. A tabela abaixo lista todas essas possibilidades, incluindo uma breve descrição de cada uma delas:

Tipo Descrição
None Nenhuma espécie de autenticação é realizada.
Windows Utiliza a autenticação baseada no Windows, fazendo com que o cliente forneça um token, representando sua credencial. Quando houver um domínio o protocolo Kerberos será utilizado, caso contrário, será utilizado o NTLM.
UserName Sendo uma das formas mais comuns, esta opção possibilita ao WCF trabalhar com um usuário e senha a serem informados pelo cliente antes de invocar a operação, validando o mesmo no próprio Windows ou em alguma tabela do banco de dados.
Certificate Permite ao cliente apresentar um certificado que o identifica.
IssuedToken Geralmente utilizado em serviços federados (serão abordados mais abaixo), issued tokes são tokens gerados por um serviço específico e, futuramente, podemos encaminhar este token para as aplicações que exigem a autenticação. Como há uma relação de confiança entre o serviço que gera o token e a aplicação, então o usuário está autenticado.
Custom Possibilita a criação customizada de um autenticador.

Para utilizar uma das opções, primeiramente é necessário analisar qual dos modos de segurança (transporte ou mensagem) você irá utilizar, pois nem todas as opções acima estão disponíveis em ambos os modos. Antes de falarmos sobre a implementação, é importante entender a relação dos tipos de credenciais e o modo de segurança e, para isso, as tabelas abaixo exibem a relação entre os modos de segurança e quais meios de fornecimento de identidade são suportados por eles, lembrando que são os bindings que expõem as propriedades utilizadas para tal configuração (em negrito é a configuração padrão):

Relação entre os bindings e a segurança baseada no transporte


Binding None Windows UserName Certificate
BasicHttpBinding Sim Sim Sim Sim
WebHttpBinding Sim Sim Sim Sim
WSHttpBinding Sim Sim Sim Sim
WSDualHttpBinding
WSFederationHttpBinding
NetTcpBinding Sim Sim Não Sim
NetPeerTcpBinding Não Não Sim Sim
NetNamedPipeBinding Não Sim Não Não
NetMsmqBinding Sim Sim Não Sim
MsmqIntegrationBinding Sim Sim Não Sim

Relação entre os bindings e a segurança baseada na mensagem


Binding None Windows UserName Certificate IssuedToken
BasicHttpBinding Não Não Sim Sim Não
WebHttpBinding
WSHttpBinding Sim Sim Sim Sim Sim
WSDualHttpBinding Sim Sim Sim Sim Sim
WSFederationHttpBinding
NetTcpBinding Sim Sim Sim Sim Sim
NetPeerTcpBinding
NetNamedPipeBinding
NetMsmqBinding Sim Sim Sim Sim Sim
MsmqIntegrationBinding

Cada binding fornece uma propriedade chamada ClientCredencialType que está acessível a partir da propriedade Transport ou Message, onde utilizaremos uma das duas, de acordo com o modo (proteção) de transferência escolhido. Para ilustrar essa configuração, os trechos de códigos abaixo exibem a forma imperativa e declarativa:

using (ServiceHost host = new ServiceHost(typeof(Servico), 
    new Uri[] { new Uri("http://localhost:9922")}))
{
    WSHttpBinding binding = new WSHttpBinding(SecurityMode.Message);
    binding.Security.Message.ClientCredentialType = MessageCredentialType.UserName;

    host.Open();
}

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <bindings>
      <wsHttpBinding>
        <binding name="ServiceBinding">
          <security mode="Message">
            <message clientCredentialType="UserName" />
          </security>
        </binding>
      </wsHttpBinding>
    </bindings>
  </system.serviceModel>
</configuration>

Para efetuar a configuração específica de um dos tipos de autenticação no serviço, devemos recorrer a uma classe chamada ServiceCredentials (namespace System.ServiceModel.Description). Essa classe fornece propriedades específicas para cada tipo de autenticação (como por exemplo: IssuedTokenAuthentication, PeerCredential, UserNameAuthentication, WindowsAuthentication), onde devemos utilizar uma delas, de acordo com o tipo de autenticação a ser realizado. Essa classe ainda implementa a Interface IServiceBehavior e, com isso, deve ser adicionada como um behavior do serviço. Abaixo é mostrado como efetuar a configuração de um serviço para que o mesmo utilize a autenticação baseada em UserName:

using (ServiceHost host = new ServiceHost(typeof(Servico), 
    new Uri[] { new Uri("http://localhost:2999/") }))
{
    WSHttpBinding binding = new WSHttpBinding(SecurityMode.Message);

    ServiceCredentials credenciais = new ServiceCredentials();
    credenciais.UserNameAuthentication.UserNamePasswordValidationMode = 
        UserNamePasswordValidationMode.Custom;
    credenciais.UserNameAuthentication.CustomUserNamePasswordValidator = new Autenticador();
    credenciais.ServiceCertificate.SetCertificate(
        StoreLocation.LocalMachine,
        StoreName.My,
        X509FindType.FindBySerialNumber,
        "3c f2 d0 c0 ef 8e 0a 96 42 36 e6 54 5f 67 50 e0");

    host.Description.Behaviors.Add(credenciais);

    host.AddServiceEndpoint(typeof(IContrato), binding, "srv");

    host.Open();
    Console.ReadLine();
}

Quando utilizamos a autenticação baseada em UserName, devemos especificar qual será o modo de autenticação e, para isso, utilizamos a propriedade UserNamePasswordValidationMode que recebe uma das três opções do enumerador UserNamePasswordValidationMode, listadas abaixo:

  • Custom: Permite especificar um validador customizado.

  • Membership: Efetuará a validação baseando-se no ASP.NET.

  • Windows: Os usernames serão mapeados para usuários Windows.

Quando utilizamos a opção Custom devemos especificar a classe que fará a validação. Para isso, devemos definir a propriedade CustomUserNamePasswordValidator com a instância da classe validadora que, obrigatoriamente, deverá herdar da classe UserNamePasswordValidator, sobrescrevendo o método Validate, assim como mostrado abaixo:

using System;
using System.IdentityModel.Tokens;
using System.IdentityModel.Selectors;

public class Autenticador : UserNamePasswordValidator
{
    public override void Validate(string userName, string password)
    {
        if (userName != "Israel" || password != "123")
            throw new SecurityTokenException("Credenciais Invalidas.");
    }
}

Observação: Para utilizar a autenticação baseada em UserName/Password que o WCF fornece sob o protocolo HTTP, será necessário utilizar um certificado. Sem a utilização deste, não seria possível garantir a integridade e confidencialidade da mensagem, comprometendo as informações e, principalmente, permitindo que alguém intercepte a mensagem e capture os dados sigilosos.

Quando o serviço é exposto utilizando algum meio de autenticação que vimos acima, ao referenciar o mesmo em um cliente, será necessário que este cliente forneça as credenciais para o serviço que, consequentemente, irá validar o cliente em algum repositório. Quando criamos o proxy, a classe que o representa herda diretamente da classe ClientBase<TChannel> que, por sua vez, disponibiliza uma propriedade chamada ClientCredentials. Essa propriedade retorna uma instância da classe ClientCredentials (namespace System.ServiceModel.Description) que possibilita ao cliente configurar as credenciais, fornecendo propriedades especificadas para cada tipo de autenticação, por exemplo: ClientCertificate, HttpDigest, IssuedToken, Peer, ServiceCertificate, UserName e Windows. Devemos apenas utilizar uma delas, de acordo com a especificação/exigência do serviço.

Como os exemplos de configuração que vimos acima foi utilizando o UserName, o código abaixo ilustra como proceder para efetuar a configuração do lado do cliente, fornecendo explicitamente o username e senha antes de invocar a operação.

using (ContratoClient proxy = new ContratoClient())
{
    proxy.ClientCredentials.UserName.UserName = "Israel";
    proxy.ClientCredentials.UserName.Password = "123";

    Console.WriteLine(proxy.Metodo());
}

A classe ServiceSecurityContext é responsável por disponibilizar informações a respeito do contexto de segurança da requisição atual. As principais propriedades que ela fornece para extrair informações a respeito do usuário autenticação são: PrimaryIdentity e WindowsIdentity. Ambas refletem a identidade do usuário autenticado, mas a segunda somente estará acessível caso a autenticação seja baseada no Windows. Caso o usuário não estiver autenticado, a propriedade PrimaryIdentity retornará uma identidade do tipo GenericIdentity em branco. Abaixo o exemplo de como acessar essas propriedades:

ServiceSecurityContext ctx = OperationContext.Current.ServiceSecurityContext;

Debug.WriteLine(string.Format("PrimaryIdentity: {0}", ctx.PrimaryIdentity));
Debug.WriteLine(string.Format("WindowsIdentity: {0}", ctx.WindowsIdentity));

Autorização

Acontecendo sempre depois da autenticação, a autorização verificará quais direitos um determinado indivíduo tem sobre o sistema. Uma vez que sabemos quem ele é, então é necessário conhecermos também quais privilégios ele tem no sistema e, a partir daí, conceder ou negar acesso à um determinado recurso. Para gerenciar isso, o WCF fornece três diferentes técnicas, a saber:

Tipo Descrição
Role-based Neste caso o acesso às operações são controlados por papéis, ou seja, o usuário (cliente) somente conseguirá ter acesso se o mesmo estiver dentro do respectivo papel. Como repositório podemos utilizar os grupos do Windows, o RoleProvider do ASP.NET ou até mesmo um repositório customizado, como é o caso de um banco de dados exclusivo.
Identity-based Conhecido como Identity Model, este modo suporta um novo modelo de autorização baseado em claims, onde você deverá analisar cada claim que estará dentro das credenciais do usuário autenticado e, assim, determinar se o usuário tem ou não acesso.
Resource-based Com esta opção os recursos são protegidos a partir das ACLs do Windows (Access Control Lists). Esta opção geralmente obriga o serviço a fazer o impersonation ou delegation (mais detalhes abaixo) do cliente antes de acessar o recurso, ficando sob responsabilidade do próprio sistema operacional verificar se o cliente tem ou não permissão de acesso.

Vimos no primeiro modo, role-based, que podemos fazer a utilização de vários repositórios, como é o caso do ASP.NET 2.0, dos grupos do Windows ou customizado. Neste caso, utilizamos as técnicas existentes dentro do .NET Framework desde sua primeira versão, onde basicamente recorremos às classes que implementam as Interfaces IIdentity e IPrincipal. A segunda gerencia a autorização expondo um método chamado IsInRole que, dado um papel, retorna um valor boleano indicando se o usuário corrente faz ou não parte do mesmo. Quando trabalhamos com credenciais Windows, então temos a classe WindowsPrincipal; já quando tratam-se de outros repositórios, como é o caso do ASP.NET, então utilizaremos a classe GenericPrincipal para o gerenciamento da autorização. Para mais detalhes sobre esses tipos, consulte o capítulo 9 deste artigo.

O primeiro passo é a configuração da autorização. Essa configuração será definida a partir de um behavior de serviço do tipo ServiceAuthorizationBehavior que irá criar e gerenciar o repositório dos papéis. Este behavior disponibiliza uma propriedade chamada PrincipalPermissionMode que aceita uma das quatro opções expostas pelo enumerador PrincipalPermissionMode e que especificará qual deles utilizar:

  • Custom: Opção que permite especificar um repositório de papéis customizado. Neste caso, a classe que representará este repositório deverá obrigatoriamente implementar a Interface IPrincipal.

  • None: A autorização baseada em papéis está desabilitada, populando a propriedade CurrentPrincipal com uma identidade em branco.

  • UseAspNetRoles: Nesta opção o repositório a ser utilizado será o RoleProvider do ASP.NET.

  • UseWindowsGroups: Utiliza os grupos do Windows para efetuar a autorização. Este é o valor padrão quando não especificado.

A configuração de qual dos tipos utilizar pode, como várias outras configurações, ser definida de forma imperativa ou declarativa, lembrando que ela deve ser configurada antes da abertura do host. Os códigos abaixo exibem como efetuar essa configuração:

using (ServiceHost host = new ServiceHost(typeof(Servico), 
    new Uri[] { new Uri("net.tcp://localhost:9922")}))
{
    host.Authorization.PrincipalPermissionMode = PrincipalPermissionMode.UseAspNetRoles;

    host.Open();
}

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <services>
      <service name="DefaultService" behaviorConfiguration="ServiceBehavior">
          <!-- Endpoints -->
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name="ServiceBehavior">
          <serviceAuthorization principalPermissionMode="UseAspNetRoles" />
        </behavior>
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel>
</configuration>

Uma vez que a configuração do repositório da autorização já está definida, chega o momento de especificarmos qual usuário ou qual papel tem acesso à uma determinada operação ou recurso. Mais uma vez, temos a possibilidade de aplicar a autorização de forma imperativa e declarativa mas, neste caso, os modos são utilizados para diferentes técnicas. O modo declarativo definirá a autorização para o método todo, enquanto o modo imperativo nos dará uma maior flexibilidade, já que podemos analisar se o usuário pertence ou não a algum papel e, consequentemente, conceder o acesso. No código abaixo primeiramente verificamos se o usuário é um administrador e, caso seja, permite o acesso a algum recurso protegido e, como dito anteriormente, podemos silenciosamente efetuar alguma operação sem disparar exceções caso ele não faça parte deste grupo.

using System;
using System.ServiceModel;

public class Servico : IContrato
{
    public string Metodo()
    {
        if (Thread.CurrentPrincipal.IsInRole("Admin"))
        {
            //faça algo
        }
    }
}

No modo declarativo utilizamos um velho conhecido: o atributo PrincipalPermissionAttribute que faz parte do namespace System.Security.Permissions. Este atributo fornece uma propriedade chamada Role onde podemos especificar o papel necessário para que o usuário tenha o direito de executar o método. Caso mais de um papel possa ter o direito de acesso ao método, então você poderá aplicar múltiplas vezes o mesmo atributo no método, variando apenas o nome do papel, assim como é mostrado abaixo:

using System;
using System.ServiceModel;

public class Servico : IContrato
{
    [PrincipalPermission(SecurityAction.Demand, Role = "Admin")]
    [PrincipalPermission(SecurityAction.Demand, Role = "Gerentes")]
    public string Metodo()
    {
        //faça algo
    }
}

Caso o usuário não faça parte de nenhum dos papéis especificados no atributo, então uma exceção do tipo SecurityException será disparada. Esse atributo funciona com qualquer modo de autorização, mas precisamos nos atentar com alguns detalhes. Quando utilizamos o ASP.NET, basicamente colocamos o nome do papel; já quando utilizamos o Windows, então é necessário especificar o domínio antes do papel, ficando algo como: “DomínioAdmin”.

A integração com o RoleProvider do ASP.NET permite aos serviços WCF fazerem uso de uma arquitetura existente e fornecida pelo ASP.NET. O recurso de Provider Model do ASP.NET foi disponibilizado a partir da versão 2.0, trazendo vários recursos que utilizam esta técnica, como é o caso do MembershipProvider (falado acima) e do RoleProvider. Para entender detalhadamente como funciona essa integração, consulte este artigo.

Para finalizar esta seção, o modo Identity-based fornece um conjunto de APIs que gerencia os claims e as policies e, baseado nestas claims, concedem ou negam o acesso. Este recurso é comumente utilizado em ambientes federados, que será comentado mais tarde, ainda neste artigo. Já no modo Resource-based, a autorização será analisada pelo próprio sistema operacional, baseando-se em ACLs previamente configuradas.

Impersonation/Delegation

O conceito de impersonation já existe há algum tempo e não é exclusividade do WCF. Esta técnica permite à aplicação assumir a identidade do usuário para atuar como ele durante o acesso a alguns recursos, como sistema de arquivos e banco de dados que estão disponíveis na mesma máquina onde está sendo executado o serviço. Já o conceito de delegation é usado para acessar recursos que estão remotos, ou melhor, fora da máquina onde está o serviço.

Podemos controlar o impersonation nas duas extremidades, ou seja, do lado do serviço e do lado do cliente. Do lado do serviço podemos recorrer à propriedade Impersonation do atributo OperationBehaviorAttribute, especificando uma das três opções expostas pelo enumerador ImpersonationOption:

  • NotAllowed: O impersonation não será executado em uma operação específica. Quando não informado, esta é a opção padrão.

  • Allowed: Caso um token Windows esteja disponível e a propriedade ImpersonateCallerForAllOperations da classe ServiceAuthorizationBehavior esteja definida como True (que por padrão é definida como False), o impersonation será executado.

  • Required: Obriga o cliente a fornecer um token Windows para efetuar o impersonation, disparando uma exceção caso o mesmo não seja informado.

O código a seguir exibe como podemos configurar uma determinada operação para requerer o impersonation. Note que por se tratar de um behavior, isso deverá ser aplicado na classe que implementa a Interface do contrato:

using System;
using System.ServiceModel;

public class Servico : IContrato
{
    [OperationBehavior(Impersonation = ImpersonationOption.Required)]
    public string Metodo()
    {
        //Implementação        
    }
}

Como vimos, a opção Allowed somente faz sentido quando você desejar “ligar” ou “desligar” o impersonation (apesar de que isso não seja uma boa prática). Quando definido como Allowed o WCF sempre irá respeitar o valor especificado na propriedade ImpersonateCallerForAllOperations, que pode ser definida de forma imperativa ou declarativa, tendo o cuidado de definir este valor antes da abertura do host. O código abaixo ilustra essas duas possíveis configurações:

using (ServiceHost host = new ServiceHost(typeof(Servico), 
    new Uri[] { new Uri("net.tcp://localhost:9922")}))
{
    host.Authorization.ImpersonateCallerForAllOperations = true;

    host.Open();
}

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <services>
      <service name="Servico" behaviorConfiguration="behaviorConfig">
        <!-- outras configurações -->
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name="behaviorConfig">
          <serviceAuthorization impersonateCallerForAllOperations="true" />
        </behavior>
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel>
</configuration>

Acima foi falado que também podemos controlar a impersonation do lado do cliente. A idéia aqui é proteger o cliente de serviços maliciosos, que pode capturar a sua credencial e executar uma operação ilegal ou que danifique algum recurso. Pode ser que em alguns casos você não queira possibilitar que o cliente forneça a sua identidade para serviço e, para customizar isso, podemos utilizar o enumerador TokenImpersonationLevel (contido no namespace System.Security.Principal), que fornece as seguintes opções:

  • None: O impersonation não será permitido.

  • Anonymous: O serviço não poderá obter a identificação do cliente e, consequentemente, não poderá efetuar o impersonation para o cliente corrente.

  • Identification: O serviço poderá obter a identificação do cliente, mas não poderá efetuar o impersonation e, se tentar efetuar, uma exceção será disparada.

  • Impersonation: Além de obter a identificação do cliente, poderá também efetuar o impersonation do cliente, mas podendo acessar somente os recursos que estão rodando na mesma máquina do serviço.

  • Delegation: Pode efetuar o impersonation do cliente e também acessar recursos que estão além da máquina onde está o serviço.

Como essa é uma configuração que diz respeito ao cliente, é do lado do mesmo que devemos efetuá-la, podendo ser de forma declarativa ou imperativa. Como isso é uma configuração exclusiva para quando a autenticação é baseada no Windows, é através desta opção que iremos acessar a propriedade AllowedImpersonationLevel que aceita uma das opções expostas pelo enumerador que vimos acima:

using System.Security.Principal;

using (ContratoClient proxy = new ContratoClient())
{
    proxy.ClientCredentials.Windows.AllowedImpersonationLevel = 
        TokenImpersonationLevel.Delegation;

    //chamada para as operações
}

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <client>
      <endpoint name="srv" behaviorConfiguration="edpBehavior" ...>
        <!-- outras configurações -->
      </endpoint>
    </client>
    <behaviors>
      <endpointBehaviors>
        <behavior name="edpBehavior">
          <clientCredentials>
            <windows allowedImpersonationLevel="Delegation"/>
          </clientCredentials>
        </behavior>
      </endpointBehaviors>
    </behaviors>
  </system.serviceModel>
</configuration>

Observação: Quando optamos por habilitar o impersonation de forma declarativa, o método todo será executado sob as credenciais do cliente, revertendo para a identidade original quando o método é retornado. Se quisermos ter um controle mais refinado sobre isso, então podemos utilizar o método Impersonate da classe WindowsIdentity, que nos permite efetuar o impersonation de forma imperativa. Quando utilizamos esta técnica, devemos utilizar o método Undo desta mesma classe para reverter a identidade.

Integridade e Confidencialidade

Felizmente o WCF fornece aos desenvolvedores a possibilidade de ter integridade e confidencialidade para o envio de mensagens. A integridade consiste em ninguém conseguir mudar o conteúdo da mensagem durante a sua viagem e, caso haja, o WCF conseguirá detectar e descartar a mesma. Já com a confidencialidade temos a garantia de que ninguém, com exceção do destinatário, conseguirá ler o que está definido no interior da mensagem. É importante dizer que a integridade não garante a confidencialidade, ou seja, a integridade apenas determina que o conteúdo não será modificado entre o cliente e o serviço, mas ele continuará legível.

Para controlar essas funcionalidades o WCF fornece dois mecanismos, onde um deles pode ser aplicado a uma operação específica enquanto o outro pode ser aplicado ao contrato como um todo. Escolher uma das alternativas dependerá do que você precisa proteger, ou seja, não vale a pena proteger todas as operações do contrato se várias delas não expõem nenhuma informação confidencial. Para ambas as alternativas podemos utilizar uma das opções expostas pelo enumerador ProtectionLevel, que está contido no namespace System.Net.Security. Esse enumerador fornece três opções:

  • None: Desabilita qualquer tipo de proteção na mensagem.

  • Sign: Apenas assina a mensagem, não criptografando-a. Essa opção irá garantir a integridade, enquanto o seu conteúdo continua legível.

  • EncryptAndSign: Criptografa e em seguida assina a mensagem, garantindo a integridade e confidencialidade.

Qualquer uma das opções escolhidas apenas afetam os dados da aplicação, ou seja, informações da infraestrutura, headers, etc., não serão impactadas. Outro detalhe importante é que quando o modo de segurança é definido como transporte, a mensagem toda será protegida pelos mecanismos fornecidos pelo transportante, não tendo qualquer efeito o uso do ProtectionLevel.

Como falado, você pode aplicar essa técnica a uma operação ou ao contrato todo e, para isso, temos a nossa disposição uma propriedade chamada ProtectionLevel, acessível através dos atributos que são utilizados para compor a mensagem e que estão em diferentes escopos: OperationContractAttribute, ServiceContractAttribute, FaultContractAttribute, MessageContractAttribute, MessageBodyContractAttribute e MessageHeaderAttribute. Vejamos através do trecho de código abaixo como podemos proceder para proteger todo o contrato:

using System;
using System.ServiceModel;
using System.Net.Security;

[ServiceContract(ProtectionLevel = ProtectionLevel.Sign)]
public interface IContrato
{
    [OperationContract]        
    string Metodo();
}

O uso desta técnica também está relacionada com o binding. Se você define explicitamente esta configuração, o binding obrigatoriamente deverá estar com a segurança habilitada. Caso você não defina explicitamente o ProtectionLevel e tiver a segurança habilitada, o WCF sempre utilizará a configuração padrão que é EncryptAndSign (salvo os bindings que não suportam segurança a nível de mensagem, como é o caso do basicHttpBinding).

Outro detalhe importante quando estamos trabalhando com criptografia é o algoritmo a ser utilizado. O WCF pode fazer uso de vários algoritmos existentes mas é importante dizer que os algoritmos devem ser iguais no serviço e no cliente para que a criptografia funcione sem problemas. Esta também é uma configuração que está debaixo do binding e deve ser especificada de forma declarativa ou imperativa. No modo declarativo, o atributo algorithmSuite recebe uma string que especifica o algoritmo, enquanto que no modo imperativo, a propriedade AlgorithmSuite espera uma instância da classe SecurityAlgorithmSuite, representando o algoritmo escolhido. No modo imperativo, utilizamos uma das propriedades estáticas disponibilizadas por esta mesma classe que retorna a instância da mesma configurada com o algoritmo escolhido. Os trechos abaixo ilustram ambas as formas de configuração do algoritmo de criptografia:

WSHttpBinding binding = new WSHttpBinding(SecurityMode.Message);
binding.Security.Message.AlgorithmSuite = SecurityAlgorithmSuite.TripleDes;

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <bindings>
      <wsHttpBinding>
        <binding name="BindingConfig">
          <security mode="Message">
            <message algorithmSuite="TripleDes" />
          </security>
        </binding>
      </wsHttpBinding>
    </bindings>
  </system.serviceModel>
</configuration>

Negociação

A “negociação” é uma funcionalidade que permite ao cliente e ao serviço negociar uma chave para efetuar a autenticação mútua e a proteção da mensagem quando o serviço está definido com a segurança baseada na mensagem. Na segurança baseada no transporte essa negociação é feita de forma automática e não conseguimos interferir neste processo.

Quando ela estiver habilitada, que é o padrão, ela elimina a necessidade do cliente conhecer antecipadamente a chave necessária para efetuar a troca das mensagens, ao contrário de quando ela estiver desabilitada, que nos obriga a ter alguns cuidados, como por exemplo: se o serviço espera por credenciais Windows, então tanto o cliente quanto o serviço devem estar dentro de um mesmo domínio; já em outras formas de autenticação, como UserName (que exige um certificado), obrigará o cliente a extrair a chave pública do certificado de uma outra forma, já que não haverá a negociação.

Há possibilidade de configurar este recurso tanto de forma declarativa quanto imperativa. No modo imperativo, recorremos à propriedade boleana NegotiateServiceCredential da classe NonDualMessageSecurityOverHttp, que estará acessível através da propriedade Security do binding. Já no modo declarativo temos o atributo boleano negotiateServiceCredential exposto através do elemento security, como é mostrado no trecho de código abaixo:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <bindings>
      <wsHttpBinding>
        <binding name="BindingConfig">
          <security mode="Message">
            <message clientCredentialType="UserName" negotiateServiceCredential="false" />
          </security>
        </binding>
      </wsHttpBinding>
    </bindings>
  </system.serviceModel>
</configuration>

Sessões Seguras

Quando uma determinada operação é invocada, o processo de autenticação sempre será realizado, verificando se o usuário é quem ele diz ser. Se, a partir de um mesmo proxy eu invoque N operações, a autenticação ocorrerá N vezes. Visando diminuir este overhead, o WCF disponibiliza um recurso chamado de sessões seguras. Em lugares onde você precisa invocar várias operações, a criação de uma sessão segura melhora consideravelmente a performance, evitando que a autenticação aconteça toda vez.

Ao invocar a primeira operação, automaticamente a sessão segura será estabelecida entre o cliente e o serviço e, para identificá-la é criado um security context token (SCT). Com isso, todas as requisições subsequentes irão utilizar este token ao invés de criar uma nova sessão e/ou refazer a autenticação. É importante dizer que, por padrão, esta sessão (token) tem 15 minutos de vida, sendo descartada se nenhuma operação for invocada neste intervalo de tempo.

Basicamente, todos os bindings que suportam a segurança baseada em mensagem (e que a utilizam) também podem fazer uso das sessões seguras. Há alguns bindings que não tem a segurança em nível de mensagem como padrão, sendo o binding NetTcpBinding um exemplo disso. Apenas o fato de definí-lo com segurança em nível de mensagem, automaticamente ele criará a sessão segura, não permitindo desabilitar este recurso. Já os outros bindings, como o WSHttpBinding, nos permitem desabilitar este recurso, através do atributo boleano establishSecurityContext, do elemento security. O trecho de código abaixo ilustra como podemos proceder para desabilitá-lo:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <bindings>
      <wsHttpBinding>
        <binding name="BindingConfig">
          <security mode="Message">
            <message establishSecurityContext="false" />
          </security>
        </binding>
      </wsHttpBinding>
    </bindings>
  </system.serviceModel>
</configuration>

Há também a possibilidade de configurar esta funcionalidade via modo imperativo. Para isso, basta recorrermos à propriedade (também boleana) EstablishSecurityContext da classe NonDualMessageSecurityOverHttp, que estará acessível através da propriedade Security do binding.

Como falado acima, o serviço cria um SCT para o cliente e, se no intervalo entre o retorno do serviço com o SCT criado e a próxima requisição o host for reinicializado (muito comum quando o serviço é exposto através do IIS), uma exceção do tipo MessageSecurityException será lançada. Isso ocorre porque do lado do serviço há uma chave que é armazenada na memória do processo e que está associada com o SCT do cliente e, uma vez perdido, ele não será mais válido.

Esse comportamento, que é o padrão, é conhecido como Stateless SCT e pode ser substituído por um Stateful SCT e, neste caso, a chave que antes ficava na memória do serviço, passa a ficar embutida no próprio SCT que é enviado para o cliente e, consequentemente, sobrevivendo às possíveis reinicializações do serviço.

Federation

Todas as configurações da autenticação e da autorização que vimos no decorrer deste artigo são realizadas em cima do serviço que estamos criando para atender uma determinada regra de negócio. Muitas vezes, ao criar um novo serviço para a mesma empresa, cria-se e configura-se novamente toda a parte de autenticação e autorização, e as vezes se tem duplicação de usuários, senhas, etc., dificultando o gerenciamento por parte da aplicação e também do usuário que a acessa.

O conceito de federação torna esse gerenciamento e segurança muito mais simples, seguro e eficaz. Os serviços apenas devem focar no desenvolvimento e execução da regra do negócio e a parte de autenticação e autorização será desacoplada do mesmo. Em um ambiente “federado”, o cliente (conhecido como Subject) deverá ser autenticado em um provedor de identidades (Security Token Service (STS)) e, finalmente, o token gerado será encaminhado para a aplicação (Relying Party (RP)).

Um outro grande benefício que temos ao utilizar os serviços “federados” é a possibilidade de estabelecer uma relação de confiança entre todos os participantes (RP), criando assim uma espécie de single sign-on (SSO). Isso quer dizer que precisamos nos autenticar uma única vez e ter acesso a todas as aplicações (RPs) que fazem parte, ou melhor, confiam em um provedor (STS) específico. Como há vários mecanismos de autenticação/autorização, o STS irá abstrair isso e as aplicações participantes passam a lidar apenas com os claims, sem a necessidade de conhecer a tecnologia utilizada para autenticação/autorização. A imagem abaixo ilustra supercialmente como este processo acontece:

Figura 1 – Participantes de serviços “federados”.
  • Fase 1: A fase 1 consiste em ir até o provedor de identidades e requerer pelo token que identifica o respectivo cliente no ambiente federado.

  • Fase 2: Se o cliente for encontrado, um token é gerado e enviado ao cliente.

  • Fase 3: A partir deste momento todas as requisições para a aplicação (ou serviço) serão realizadas de forma segura, contendo o token gerado pelo provedor que todos confiam.

  • Fase 4: Depois do token validado o serviço permite a execução da requisição e retorna a resposta para o cliente que a solicitou.

“Geneva”

Atualmente conhecido como “Geneva”, é o codename para um novo framework que a Microsoft está criando para facilitar a criação de aplicações e serviços baseados em claims e para implementação de serviço “federados”. Os recursos fornecidos por esta nova API vai desde a possibilidade de que aplicações (ASP.NET, Windows, etc.) ou serviços possam utilizar o modelo baseado em claims até a construção de nossos próprios STS.

Este framework está dividido em três grandes partes: “Geneva” Server, CardSpace “Geneva” e “Geneva” Framework. Infelizmente não há espaço para falar sobre essa API neste momento, pois ela precisa de um artigo exclusivo para abordar os problemas conhecidos e como ela pode ser utilizada para sanar cada um deles.

Conclusão: Apesar de um pouco grande, este artigo tentou cobrir as principais funcionalidades fornecidas pelo WCF para o gerenciamento de autenticação e autorização de serviços, abordando as vantagens e desvantagens de cada modo. Analisamos outras características ligadas diretamente à autenticação, como foi o caso do impersonation e o delegation e, além disso, vimos como proceder para proteger as mensagens, garantindo a integridade e confidencialidade. Para finalizar, foi falado superficialmente sobre Federation e o “Geneva” que são temas importantes e que serão esgotados em futuros artigos.

Seguranca.zip (59.55 kb)

commonBehaviors

commonBehaviors é uma seção que pode somente ser definida no arquivo machine.config. Essa seção permite adicionarmos behaviors para endpoints (endpointBehaviors) e para serviços (serviceBehaviors), sendo a coleção de endpointBehaviors carregada por aplicações cliente, enquanto a coleção de serviceBehaviors são aplicadas apenas nos serviços. Lembrando que isso somente vale para aplicações (clientes ou serviços) que correm na máquina onde esses behaviors foram configurados.

Com mais este recurso, é possível aplicar alguma regra (tanto para cliente quanto para o serviço) para efetuar alguma espécie de validação ou logging. Apenas atente-se para decorar o assembly/classe onde este behavior foi criado como o atributo AllowPartiallyTrustedCallersAttribute para que, aplicações que estão rodando sob partial trust, possam fazer uso deles.

Poison Queues e a ordem de entrega

Já falei neste post e neste artigo os benefícios da utilização das poison queues. Mas utilizá-la nem sempre uma boa alternativa. Quando habilitada, mensagens que repetidamente falham (independentemente do problema ocorrido) durante o seu processamento, são movidas para esse tipo de fila, permitindo assim que as mensagens subsequentes sejam processadas.

Essa alternativa somente é válida quando voce não precisa garantir o processamento ordenado da fila, ou seja, uma vez que a mensagem é movida para a poison queue a próxima mensagem é processada, e com isso perdemos a característica principal de uma fila, que é o processamento baseado no padrão FIFO (First-In, First-Out).

Quando a ordem de processamento das mensagens for importante, não podemos habilitar a poison queue. Neste caso, quando um problema acontece, será necessária a intervenção de alguma pessoa (técnico) que consiga resolver o problema e dar continuidade no processamento sequencial da fila. Quando o problema se tratar de algo relacionado à infraestrutura, como acesso à banco de dados, permissões, etc., basta resolve-los e seguir em frente; já quando o problema é alguma falha no conteúdo da mensagem, então compete ao usuário que está analisando-a decidir excluí-la ou não.

Cookies em serviços HTTP

Bindings baseados em HTTP, como por exemplo BasicHttpBinding ou WSHttpBinding, possuem uma propriedade booleana chamada AllowCookies. O nome desta propriedade pode parecer confuso, pois a finalidade dela não é permitir que o binding utilize ou não cookies, mas sim determinar quem deverá manipulá-los.

Quando esta propriedade estiver definida como True, o próprio runtime do WCF irá gerenciar os cookies. Isso quer dizer que ele criará uma instancia da classe CookieContainer e a associará ao serviço, utilizando-a para todas as requisições, ou seja, todo e qualquer cookie retornardo pelo serviço será automaticamente reenviado nas futuras requisições, sem a necessidade de controlar como isso é realizado.

Já quando a propriedade AllowCookies for definida como False (que é o padrão), o WCF não irá gerenciar os cookies. Isso quer dizer que entre as chamadas das operações do lado do cliente, será necessário que voce recupere os possíveis cookies retornados pelo serviço e os adicione novamente para efetuar uma posterior requisição. Como podemos perceber, ao contrário da opção anterior, devemos controlar totalmente a captura, manipulação e o envio para o serviço. Voce deve ter uma atenção especial aqui, já que uma das características do WCF é ser independente de protocolo e, se dentro da implementação do serviço voce tentar manipular essas informações (exclusivas do HTTP), voce começará a ter uma afinidade com tal protocolo.

Clock Skew

Quando construímos um serviço WCF, basicamente criamos o serviço e os clientes que o acessam. Fazemos a distribuição dos clientes, sem nos preocuparmos com uma configuração que, as vezes, podem resultar em possíveis erros durante a execução.

Ao expor um serviço através de um binding que já traz segurança integrada (como é o caso do WSHttpBinding), há um comportamento especial em que ao enviar a mensagem do cliente para o serviço, a data/hora atual são incluídos dentro da requisição. Quando a mensagem chegar para o serviço, ele analisará essa data/hora antes de efetivamente executar o pedido. Para evitar possíveis ataques de replay, o WCF verifica se a data/hora de criação é maior ou menor que o permitido (o padrão é 5 minutos de tolerancia para mais ou para menos) e, caso seja, a requisição será rejeitada.

Para customizar esse valor, podemos recorrer a propriedade MaxClockSkew que aceita um TimeSpan que especifica a tolerancia máxima permitida. Para os bindings pré-definidos essa configuração não está acessível, tendo que fazer o uso de um customBinding caso necessitamos mudar a configuração padrão que, dificilmente será necessário, a menos que a sincronização dos relógios das duas ou mais máquinas envolvidas não seja possível.

O futuro do System.Messaging.dll

Juntamente com o Windows Vista e Windows Server 2008 foi colocado a disposição a nova versão do Message Queue, 4.0. Essa nova versão traz algumas funcionalidades muito interessantes e, a primeira delas, são as subqueues. Como o próprio nome diz, as subqueues são filas que estão abaixo da fila principal e herdam as suas características. Não podemos mandar mensagens explicitamentes para essas filas, mas podemos remove-las de lá. Além disso, não temos o controle da criação ou exclusão dessas subqueues, ficando sob responsabilidade do próprio Message Queue que a criará quando preciso e a excluirá quando estiver vazia.

Outra funcionalidade muito interessante são as Poison Message Queues. Mensagens que estão dentro de uma fila estão propícias a falharem e, se essa falha ocorrer repetidamente, podemos ter um loop infinito, ou seja, a mensagem atual nunca será processada e, consequentemente, as mensagens subsequentes também não. Para isso, o Message Queue 4.0 possibilita especificarmos a quantidade de tentativas de processamento e, caso todas elas esgotem, a mesma será movida para uma Poison Message Queue que, nada mais é que uma subqueue, dando sequencia no processamento das mensagens seguintes.

Apesar de muito interessantes, essas novas funcionalidades somente podem ser utilizadas a partir do WCF (mais detalhes aqui). Pelo fato da Microsoft estar centralizando todas as formas de comunicação em cima do WCF, acredito que a API System.Messaging não evoluirá e, consequentemente, não suportará essas novas funcionalidades.

Extraindo o IP do cliente

Em algum momento, talvez gostaríamos de extrair o endereço (IP) do cliente que efetuou a requisição para um serviço, com a finalidade de logging, monitoramento, etc. Felizmente há uma possibilidade para resgatar essa informação e, podemos acessá-la durante a execução de uma operação de um serviço exposto via WCF.

Para isso, há uma classe chamada RemoteEndpointMessageProperty que possui duas propriedades: Address e Port. A instancia desta classe é fornecida pelo contexto da requisição, através da coleção IncomingMessageProperties. Para exemplificar o seu uso, veja o código abaixo:

RemoteEndpointMessageProperty cliente =
    OperationContext.Current.IncomingMessageProperties[RemoteEndpointMessageProperty.Name]
        as RemoteEndpointMessageProperty;

Console.WriteLine(“{0}:{1}”, cliente.Address, cliente.Port);

Como especificado na documentação, essas informações só estarão disponíveis quando o serviço estiver exposto através do protocolo HTTP ou TCP. Além disso, quando a requisição já foi efetuada (como é o caso de operações one-way), essas informações estarão indisponíveis. E, para finalizar, se a requisição passar por intermediários (roteadores), o endereço/porta irá refletir o endereço/porta do último intermediário.