Página de Acesso Negado

Quando utilizamos FormsAuthentication para proteger uma determina página ou diretório, há um comportamento que talvez não agrade a maioria das pessoas. Suponhamos que você faça o login digitando o seu nome de usuário e senha. Ao validar esse usuário em algum repositório, o ASP.NET automaticamente redireciona o usuário para a página/diretório que ele tentou acessar inicialmente.

A partir daí, se você utilizar roles para restringir o acesso à alguma página/diretório dentro da aplicação e o usuário não esteja dentro dela, ele será redirecionado – novamente – para a página de login. Talvez isso não seja o ideal, já que o mais coerente seria redirecionar o usuário para uma página que contenha uma mensagem mais amigável e próxima ao que realmente aconteceu, algo como: “Você não tem permissão para acessar o recurso solicitado. Contate o Administrator.”.

Quando utilizamos a UrlAuthorization (que já é o padrão), a cada requisição ela verifica se o usuário que acessa uma determinada página, possui a permissão para isso. Caso a configuração no arquivo Web.config diz que, para acessar aquela página, é necessário que o usuário esteja na role de “Administradores”, e ele por sua vez não estiver, o UrlAuthorization define a propriedade StatusCode da resposta do HTTP para 401 (que significa “Acesso Negado”). O FormsAuthentication (mais precisamente, o FormsAuthenticationModule) se vincula ao evento EndRequest, e dentro dele analisa se a propriedade StatusCode foi definida como 401. Caso tenha sido, o FormsAuthenticationModule redireciona o usuário para a página de login, trocando o StatusCode para 302 (“Redirecionar”).

Para mudar este comportamento, podemos criar um módulo e nos vincularmos ao evento EndRequest. Dentro deste evento, podemos analisar se a propriedade StatusCode foi definida como 401, e se estiver, redirecionar para uma página que informa exibe a mensagem correta ao invés da página de login, melhorando assim a navegabilidade da aplicação. O módulo é extremamente simples, assim como podemos notar abaixo:

public class AccessDeniedModule : IHttpModule
{
    public void Init(HttpApplication context)
    {
        context.EndRequest += new EventHandler(context_EndRequest);
    }

    private void context_EndRequest(object sender, EventArgs e)
    {
        HttpApplication app = (HttpApplication)sender;

        if (app.Response.StatusCode == 401 && app.User.Identity.IsAuthenticated)
        {
            app.Response.Redirect(“~/AreaRestrita/AcessoNegado.aspx”);
        }
    }

    public void Dispose() { }
}

Como já sabemos, para que um módulo funcione, precisamos acoplá-lo na execução do ASP.NET, e fazemos isso através do elemento <httpModules />. Mas este módulo deve ter um cuidado especial para que funcione adequadamente. Já existem diversos módulos em execução pelo ASP.NET, e um deles é justamente o FormsAuthenticationModule. Se simplesmente adicionarmos o módulo que criamos na coleção de módulos da aplicação, o StatusCode vai chegar sempre como 302, pois a requisição primeiramente passará pelo módulo FormsAuthenticationModule, que como vimos acima, faz essa mudança. Com isso, a nossa condicional sempre irá falhar, fazendo com que o usuário seja redirecionado para a página de login. A dica aqui é ir até o arquivo Web.config que está em nível de servidor (%windir%Microsoft .NETFrameworkVERSÃOCONFIG), copiar a coleção de módulos e colar no arquivo Web.config da aplicação, como podemos ver abaixo:

<httpModules>
  <clear />
  <add name=”ScriptModule”
       type=”System.Web.Handlers.ScriptModule, System.Web.Extensions”/>
  <add name=”OutputCache”
       type=”System.Web.Caching.OutputCacheModule”/>
  <add name=”Session”
       type=”System.Web.SessionState.SessionStateModule”/>
  <add name=”AccessDeniedModule”
       type=”AccessDeniedModule”/>

  <add name=”FormsAuthentication”
       type=”System.Web.Security.FormsAuthenticationModule”/>
  <add name=”RoleManager”
       type=”System.Web.Security.RoleManagerModule”/>
  <add name=”UrlAuthorization”
       type=”System.Web.Security.UrlAuthorizationModule”/>
  <add name=”AnonymousIdentification”
       type=”System.Web.Security.AnonymousIdentificationModule”/>
  <add name=”Profile”
       type=”System.Web.Profile.ProfileModule”/>
</httpModules>

Depois de colocar os módulos no arquivo Web.config da aplicação, precisamos colocar o elemento <clear /> para que não duplique a execução dos módulos. E para finalizar, o módulo que criamos deverá ser colocado antes do módulo do FormsAuthentication para que funcione como o esperado. Aproveitando a oportunidade, é importante que você deixe somente os módulos que a sua aplicação realmente utiliza, ganhando alguma performance, já que evitará passar por passos desnecessários. Para maiores detalhes sobre isso, consulte este artigo.

A sua sessão expirou

Um amigo me pediu uma ajuda para escrever a seguinte mensagem na página de Login de uma aplicação ASP.NET: “A sua sessão expirou. Efetue o login novamente.”. Como ele está utilizando FormsAuthentication, pensei em analisar a existência da QueryString “ReturnUrl”, que geralmente é adicionada como parte da URL, quando você tenta acessar algum recurso protegido e que você não esteja devidamente autenticado.

O problema desta técnica é que as vezes o usuário tem o endereço (nos “Favoritos”, por exemplo) desta página restrita, e suponhamos que ele abra o navegador e tente acessá-la diretamente. O FormsAuthentication fará a parte dele, ou seja, redirecionará o usuário para a página de Login com a QueryString “ReturnUrl” contendo a página que ele tentou acessar. Se estiver analisando essa QueryString, a mensagem de “sessão expirada” será exibida, mas não faz sentido neste caso, já que é o primeiro acesso deste usuário.

Para tentar contornar esse problema, a solução foi a criação de um módulo, vinculando ao evento BeginRequest. Esse módulo será responsável por analisar a validade do cookie de autenticação, e se estiver expirado, redirecionará o usuário para a página de Login. Esse mesmo processo acaba sendo feito pelo próprio FormsAuthenticationModule, mas com este novo módulo, a ideia é nos antecipar ao FormsAuthentication e adicionar um novo item na coleção de QueryStrings, que especifica que a autenticação expirou. Esse valor será analisado pela página de Login, que baseando-se nele irá ou não exibir a mensagem em questão. Abaixo temos o módulo na íntegra:

using System;
using System.Web;
using System.Web.Security;

public class AuthenticationExpiredModule : IHttpModule
{
    private HttpApplication _app;

    public void Init(HttpApplication context)
    {
        this._app = context;
        this._app.BeginRequest += new EventHandler(this._app_BeginRequest);
    }

    private void _app_BeginRequest(object sender, EventArgs e)
    {
        if (this._app.Request.Path != FormsAuthentication.LoginUrl)
        {
            HttpCookie authCookie = 
                this._app.Request.Cookies[FormsAuthentication.FormsCookieName];

            if (authCookie != null)
            {
                FormsAuthenticationTicket ticket =
                    FormsAuthentication.Decrypt(authCookie.Value);

                if (ticket.Expired)
                {
                    this._app.CompleteRequest();
                    FormsAuthentication.RedirectToLoginPage(“Authentication=Expired”);
                }
            }
        }
    }

    public void Dispose() { }
}

Depois de criado, precisamos registrá-lo no arquivo de configuração (Web.config) para que ele comece a fazer parte da execução:

<httpModules>
    <add
        name=”AuthenticationExpiredModule”
        type=”AuthenticationExpiredModule”/>
</httpModules>

E, finalmente, já na página de Login, verificando a existência da QueryString:

this.AuthenticationExpiredMessage.Visible = Request.QueryString[“Authentication”] == “Expired”;

Utilizando o EF em Ambiente Parcialmente Confiável

Estava utilizando a template de projeto Web Site para a criação de um site com uma vida curta, e que se destina a realizar uma tarefa temporária. Como ele deve acessar uma fonte de dados SQL Server para manipular alguns dados, optei por utilizar o Entity Framework como forma de acesso, ao invés do LINQ To SQL ou até mesmo do ADO.NET.

Em pouco tempo essa aplicação ficou pronta e tudo funcionava tranquilamente na máquina de desenvolvimento, até que decidi – erroneamente – configurar o Web.Config localmente. Entre essas configurações, uma delas foi ajustar o nível de segurança que a aplicação deverá rodar, que – teoricamente – não preciso nada mais do que o nível “Medium”. Sendo assim, o meu arquivo Web.Config passou a ficar com a seguinte entrada:

<trust level=”Medium” />

Ao tentar recompilar a aplicação no Visual Studio .NET, me deparo com o seguinte erro listado na IDE:

Type ‘System.Data.Entity.EntityDesignerBuildProvider’ cannot be instantiated under a partially trusted security policy (AllowPartiallyTrustedCallersAttribute is not present on the target assembly).

Ao abrir o arquivo Web.Config da aplicação, você notará que no elemento connectionStrings possuirá as referências para os arquivos que possuem as descrições das entidades e mapeamentos (CSDL, MSL e SSDL), acrescentado o prefixo “res://”, que indica que eles serão adicionados ao assembly como Resource.

Além disso, você verá que existe um builder provider do tipo EntityDesignerBuildProvider, vinculado à aplicação. Essa classe é responsável por extrair as informações dos arquivos mencionados acima, e modificar os assemblies que estão sendo gerados, embutindo-as como Resources. Esse processo não pode ser executado em ambiente parcialmente confiável, já que a permissão necessária não será concedida à aplicação. Veja que a mensagem de erro informa que o Assembly onde está declarado este builder provider, não pode ser chamado por aplicações parcialmente confiáveis, pois a ausência do atributo AllowPartiallyTrustedCallersAttribute evita isso.

A transformação do arquivo EDMX (CSDL, MSL e SSDL) ocorre somente na primeira vez que se compila a aplicação (%windir%Microsoft.NETFrameworkVersaoTemporary ASP.NET Files), e se nesse momento estiver com ela configurada como parcialmente confiável, assim como eu fiz acima, você irá obter o erro em questão. Se você optar por pré-compilar a aplicação em ambiente “FullTrust”, e fazer o deployment em ambiente parcialmente confiável, você não terá este problema. Isso é perfeitamente possível, já que as configurações de uma aplicação (Web.Config) não são compiladas.

Uma outra alternativa é utilizar o conceito de sandboxing, onde você isola o EDMX em uma Class Library, e faz referencia para ela no projeto Web. Como o arquivo EDMX estará embutido na DLL gerada, você não precisa mais das referências aos arquivos CSDL, MSL e SSDL na aplicação Web, e muito menos do build provider EntityDesignerBuildProvider no Web.Config. Neste caso, o ponto negativo é ter que gerenciar dois projetos e Assemblies.

Ainda há uma última alternativa neste caso, onde você extrai os arquivos CSDL, MSL e SSDL a partir do EDMX, e modifica a connectionString para apontar fisicamente para estes arquivos, que devem estar na mesma aplicação (talvez no diretório bin). Particularmente prefiro a opção da geração do Assembly a parte, que facilita a reutilização por várias aplicações e não corremos o risco de alguém, acidentalmente, excluir estes arquivos que são necessários para o Entity Framework trabalhar.

Security-Transparent Code

Uma das grandes inovações que o .NET trouxe é a capacidade de conceder ou negar acesso à uma determinada aplicação (Assembly), independentemente do usuário que está acessando-a, sendo ou não um “Administrador” da máquina onde a aplicação está sendo executada. Esse novo conceito, presente desde a primeira versão do .NET Framework, é chamada de Code Access Security – CAS.

Esse recurso é extremamente rico em detalhes, onde podemos customizar quais direitos a aplicação terá baseando-se em condições que são avaliadas durante a execução da mesma.  Para compor o CAS, temos vários objetos, tais como: Permissions, Permissions Sets, Security Levels, Membership Conditions, Demands, etc. Ao mesmo tempo que isso é extremamente poderoso, a sua configuração torna-se complexa demais, principalmente quando você não tem um conhecimento intermediário sobre o seu funcionamento.

Um dos principais cenários onde precisamos fazer o uso destas funcionalidades, é quando estamos criando libraries (DLLs) para serem consumidas em ambientes parcialmente confiáveis, ou seja, você cria uma DLL para encapsular o acesso ao banco de dados, sistema de arquivos, etc., e quer consumí-la em uma aplicação que não está sendo executada em “FullTrust” (o que muitas vezes acontece). Nesses casos, se gasta mais tempo testando e configurando as políticas com seus respectivos níveis de segurança, do que na tarefa que a DLL irá executar.

A partir do .NET Framework 2.0, a Microsoft trouxe um recurso chamado de Security-Transparent Code. Essa funcionalidade facilita a escrita e configuração de DLLs que serão consumidas por aplicações que rodam em ambientes parcialmente confiáveis. A ideia desta técnica é você refatorar o código (seja em assemblies, classes ou métodos) em “código transparente” e “código crítico”, com o propósito de separar o código que roda como parte da aplicação do código que roda como parte da infraestrutura, criando uma espécie de barreira entre essas duas seções.

O “código transparente” não fará nada (do ponto de vista da segurança) além do que lhe foi concedido, ou seja, não acessará nenhum tipo de código que exija a elevação dos privilégios atuais. Ao precisar executar uma tarefa que exige um nível de segurança mais elevado, entre em cena o “código crítico”, que rodará em “FullTrust”. Neste caso, o “código transparente” irá invocar algum membro do “código crítico”, que possuirá todos os privilégios necessários, independentemente de quais o cliente possua, evitando elevar os privilégios do “código transparente”.

Os códigos marcados como “transparente” terão algumas restrições, tais como: elevar os privilégios (Asserts) que lhe foram concedidos; efetuar Link Demands (tudo será transformado em “full demand”) e qualquer código unsafe que ele eventualmente execute em “código transparente”, também efetuará uma “full demand”.

Observação: A diferença entre Link Demand e Demand (ou Full Demand) é que a primeira ocorre somente em tempo de compilação (JIT) e apenas irá verificar se o chamador imediato tem a permissão requerida. Já a segunda opção, Demand, executará a stack walk, ou seja, irá percorrer todos os elementos da stack, verificando se todos eles possuem a permissão solicitada.

Depois desta reestruturação (física ou virtual), a CLR irá assegurar (em tempo de execução) que o “código transparente” não poderá efetuar qualquer elevação de privilégios, garantindo que tudo o que o ele tentar executar, exigirá que toda a call stack tenha a respectiva permissão. E, para customizar a comportamento desta funcionalidade, a Microsoft disponibiliza alguns atributos:

  • SecurityTransparentAttribute: Somente permitido em nível de Assembly, diz ao runtime que ele será “transparente”, ou seja, não poderá executar qualquer ação que exija a elevação de privilégios. É importante dizer que “código transparente” não pode invocar “código crítico”.
  • SecurityCriticalAttribute: Quando utilizado em nível de Assembly, indica que ele pode ter “código crítico”. Isso vai depender da opção que você define ao utilizar o enumerador SecurityCriticalScope. A opção Everything indica que todo o código será considerado “código crítico”, enquanto a opção Explicit, determina que somente o membro onde o atributo está sendo aplicado será considerado como “código crítico”.
  • SecurityTreatAsSafeAttribute: Indica que o membro onde este atributo está sendo aplicado, pode ser acessado por outros membros que estão marcados com os atributos SecurityTransparentAttribute ou AllowPartiallyTrustedCallersAttribute. Não há nenhuma validação que evite a compilação quando você aplica este atributo em métodos públicos, e se você fizer isso, estes membros podem ser acessados através do “código transparente” tendo ter possíveis vulnerabilidades.

No exemplo abaixo, estamos definindo o atributo SecurityCriticalAttribute em nível de Assembly, dizendo que ele poderá conter “código crítico”. Com isso, somos obrigados a determinar qual método será “crítico”, utilizando o atributo SecurityCriticalAttribute ou o SecurityTreatAsSafeAttribute. Neste caso, optamos pela segunda opção, pois desejarmos que o método ReadFile possa ser invocado a partir de membro internos bem como externos, e com a configuração abaixo, será considerado “transparente”, e sem este atributo não seria possível.

[assembly: SecurityCritical]

static void Main(string[] args)
{
    Console.WriteLine(ReadFile());
}

[SecurityCritical]
[SecurityTreatAsSafeAttribute] 
private static string ReadFile(string filename)
{
    new FileIOPermission(FileIOPermissionAccess.Read, filename).Assert();
    //Ler o arquivo
}

IIS, WCF e Partial Trust

Há algum tempo eu falei sobre a possibilidade de invocar serviços WCF em aplicações parcialmente confiáveis. Mas ainda há um segundo cenário, que é a exposição de serviços através de aplicações ASP.NET Web Site/WCF Service. Para criar um serviço WCF, você não precisa necessariamente criar um tipo de projeto exclusivo como o WCF Service. Um projeto do tipo ASP.NET Web Site pode, tranquilamente, servir como host de um serviço WCF, e para isso, basta adicionar um arquivo *.svc e configurá-lo corretamente no Web.config.

Independentemente de qual das alternativas utilize, você poderá se deparar com uma restrição de segurança, e dependendo das funcionalidades (mais precisamente do binding e seus elementos) que utiliza, o serviço não rodará. Isso muitas vezes acontece quando você faz o deployment para um servidor, em que a configuração padrão do .NET Framework foi alterada (visando uma maior segurança). Uma das regras mais importantes que se deve ter ao configurar um servidor Web (IIS), é não permitir que as aplicações que rodem ali executem em “Full Trust”. Para isso, se altera o arquivo Web.config (que está em nível de servidor), definindo o atributo level do elemento trust para “Medium” ou qualquer nível abaixo disso.

Neste ambiente, você poderá utilizar os bindings BasicHttpBinding, WSHttpBinding ou o WebHttpBinding, desde que eles estejam com a segurança desabilitada ou com a proteção em nível de transporte. O binding WSDualHttpBinding também não pode ser utilizado neste cenário, já que algumas tarefas que ele desempenha exige um nível de segurança mais elevado. Finalmente, para tentar resolver este problema, podemos fazer uso das técnicas mostradas pelo Juval Lowy neste artigo, disponibilizando alguns helpers para facilitar a criação de hosts em ambientes parcialmente confiáveis.

Migrando de ASMX para WCF

Junto com a primeira versão do Visual Studio .NET e do .NET Framework, temos a possibilidade de criarmos serviços Web, baseados em XML e utilizando a tecnologia ASP.NET Web Services (ASMX). Isso ainda continua disponível nas templates de projeto da versão mais atual do Visual Studio .NET, mas, para a criação de novos projetos, ou melhor, de novos serviços, o ideal é recorrer ao WCF – Windows Communication Foundation.

De qualquer forma, os ASP.NET Web Services já existem há algum tempo e há muitas aplicações que ainda o utilizam, e este artigo ajudará a entender melhor as diferenças entre ASMX e o WCF, desde a sua estrutura de projeto até detalhes relacionados à execução do mesmo. Cada uma das seções a seguir irá analisar e discutir essas mudanças, falando também sobre alguns detalhes importantes que, se não se atentar, poderá ter um comportamento “estranho” durante a execução.

Templates e Estrutura de Projeto

Quando você opta por criar um projeto ASMX, então você deve recorrer à template ASP.NET Web Service Application. Ao criar esse tipo de projeto, você poderá adicionar dentro dele arquivos com extensão *.asmx. Esse tipo de arquivo representará um serviço. Dentro desta classe, teremos os métodos que queremos publicar. Vale lembrar que o modificador de acesso do método (public, private, etc.) não tem validade. O que determinará a visibilidade do método é se ele estiver ou não decorado com o atributo WebMethodAttribute.

Além disso, a classe que representa o serviço pode, opcionalmente, herdar da classe WebService. Essa classe fornece acesso direto aos objetos ASP.NET, como Application e Session. Assim como nas aplicações ASP.NET tradicionais (UI), o arquivo ASMX apenas possui uma diretiva chamada @WebService, que define alguns atributos utilizados pelo compilador do ASP.NET. As classes necessárias para trabalhar com o ASMX estão contidas no Assembly System.Web.Services.dll e debaixo do namespace System.Web.Service.

Já com o WCF, trabalhamos de forma bem parecida. Para trabalhar com ele, é necessário que você, ao criar um novo projeto, escolha a template WCF Service Application. Neste projeto, adicionaremos arquivos com extensão *.svc, que representará o serviço. Esse tipo de arquivo também possui uma diretiva própria, chamada de @ServiceHost, que também leva informações ao compilador do ASP.NET. As classes necessárias para trabalhar com o WCF estão contidas no Assembly System.ServiceModel.dll e debaixo do namespace System.ServiceModel.

Apesar de existirem templates distintas para cada uma das tecnologias, isso não quer dizer que você está obrigado a criar um projeto específico para os teus serviços. Caso você já possua uma aplicação ASP.NET criada, é perfeitamente possível adicionar arquivos com extensão *.asmx ou *.svc neste projeto. Uma aplicação ASP.NET (UI) consegue coexistir com qualquer uma dessas tecnologias.

Contratos

O ASMX irá se basear nos métodos decorados com o atributo WebMethodAttribute para gerar o documento WSDL. Você deverá controlar a visibilidade dos teus métodos adicionando um removendo este atributo. Qualquer tipo complexo referenciado nos métodos, será automaticamente inserido na descrição do serviço sem nenhuma configuração extra.

Já o WCF trabalha de forma bem diferente. Ele utiliza interfaces para determinar os contratos que o serviço possui. Essas interfaces são aquelas tradicionais, que já utilizamos no nosso dia-à-dia, mas decorada com um atributo chamado ServiceContractAttribute. Dentro das interfaces teremos os métodos, e controlamos se eles serão ou não expostos através do atributo OperationContractAttribute.

Em relação a tipos complexos vinculados ao contrato do serviço, a exposição deles será determinado pela versão do WCF que está utilizando. Se estiver utilizando a versão 3.0, então você precisará decorar essas classes com o atributo DataContractAttribute, e para cada propriedade que desejar expor, decorá-la com o atributo DataMemberAttribute (opt-in). Com o Service Pack 1 para o .NET Framework 3.5, esses parâmetros são opcionais (opt-out), tornando-os POCOs. Mas há um detalhe: quando você decorar a classe com o atributo DataContractAttribute, então você deverá explicitamente determinar quais propriedades deseja disponibilizar, utilizando o atributo DataMemberAttribute.

Há um outro detalhe importante durante a execução das operações. Quando você utiliza ASMX, ele concatena o namespace com o nome da mensagem para determinar o Action. O WCF concatena o namespace, o nome do serviço e o nome da operação. Para manter compatibilidade com possível clientes ASMX, você deve manter a mesma fórmula, e para isso, pode recoorer a propriedade Action do atributo OperationContextAttribute.

Serialização/Deserialização

A serialização e deserializaão estão condicionadas ao serializador que cada tecnologia utiliza. O ASMX utiliza o XmlSerializer (System.Xml.Serialization) para transformar os objetos em XML e vice-versa. O XmlSerializer serializa todos os membros públicos (propriedades e campos), sem a necessidade de definir algum atributo. Você ainda pode controlar como essa serialização será realizada, utilizando vários atributos que existem debaixo deste mesmo namespace. Para maiores detalhes sobre o funcionamento do XmlSerializer e dos atributos, consulte este artigo.

O WCF, por outro lado, utiliza o serializador DataContractSerializer por padrão. Este serializador trabalha de forma semelhante ao XmlSerializer, com poucos diferenças. Entre essas diferenças temos o entendimento por parte do DataContractSerializer do atributo SerializableAttribute, para manter a compatibilidade com objetos que foram criados para serem utilizados pelo .NET Remoting. Além disso, uma outra diferença é a capacidade que este serializador tem de persitir também membros definidos como private e protected. Este serializador ainda gera um XML mais simplificado, melhorando a interoperabilidade entre as plataformas. Se desejar utilizar o XmlSerializer, então basta decorar o seu contrato com o atributo XmlSerializerFormatAttribute. Somente utilize o XmlSerializer para cenários onde você precisa ter controle total sob como o XML é gerado.

Ainda temos um terceiro serializador que é o NetDataContractSerializer. A diferença em relação ao DataContractSerializer é que ele armazena no XML gerado, informações a respeito do tipo (CLR), como versão, assembly e full name. Este serializador é rico nas informações referente ao tipo, ele compartilha tipos, ao contrário do DataContractSerializer, que compartilha contratos. Este ainda possui baixa interoperabilidade, e pode ser utilizado em cenários onde se tem .NET Remoting.

Protocolo/Hosting

O ASMX pode somente ser hospedado no IIS, utilizando o protocolo HTTP/HTTPS. Já o WCF tem uma arquitetura muito mais flexível e é independente do protocolo, ou seja, ele pode rodar em HTTP, HTTPS, TCP, MSMQ, etc. Isso quer dizer que ao construir um serviço através do WCF, você não deve mencionar e/ou confiar em nenhum momento que o mesmo será exposto através de um determinado protocolo, já que você não conseguirá garantir isso.

O WCF também pode utilizar o IIS como host. Mas além dele, podemos fazer uso de outras aplicações para expor um serviço, como um Windows Service, ou ainda, uma simples aplicação Windows/Console. Para mais detalhes sobre cada um dos tipos de host, consulte este artigo.

Extensibilidade

O ASMX permite você interceptar as requisições através de SOAP Extensions. Com elas, podemos acoplar um código customizado no processamento da mensagem, efetuando logs, tratando a autenticação/autorização, etc. A estrutura para a criação de um extensão é simples: basta herdar da classe SoapExtension e implementar o método ProcessMessage. Como parâmetro, este método traz uma propriedade chamada Stage, que podemos identificar o estágio do processamento da mensagem. Ela nos fornece quatro opções auto-explicativas: BeforeSerialize, AfterSerialize, BeforeDeserialize e AfterDeserialize. Para utilizá-la, basta criar um atributo que herda de SoapExtensionAttribute, e sobrescrever o método ExtensionType.

O WCF traz inúmeros pontos de extensibilidade tanto do lado do serviço quanto do cliente. Através destes pontos, podemos interceptar e inspecionar os parâmetros que estão sendo enviados, a escolha da operação a ser disparada, a criação da instância da classe que representa o serviço, a serialização e deserialização da mensagem (é o que a classe SoapExtension faz), entre outros. O WCF fornece vários tipos (interfaces e classes) para você customizar o que for necessário. Para entender detalhadamente sobre todos as possibilidades que temos, consulte este artigo.

Segurança

O ASMX pode confiar somente na segurança baseada no transporte, ou seja, ele somente será seguro se você expor o serviço através de HTTPS. Você somente conseguirá abrir mão do HTTPS se utilizar a segurança baseada na mensagem, que está disponível no ASMX através do WSE – Web Services Enhancements. Muitas vezes se utiliza um SoapHeader com usuário e senha. Isso somente terá alguma segurança se utilizar HTTPS ou segurança em nível de mensagem. Do contrário, qualquer um que intercepte a requisição, conseguirá extrair o conteúdo da mensagem e seus respectivos headers.

Como já era de se esperar, o WCF fornece ambos níveis de segurança nativamente. São configurações que você realiza (de forma imperativa ou declarativa), e que o serviço utilizará para efetuar a autenticação e autorização do cliente. Uma das grandes dificuldades que o pessoal encontra ao utilizar o WCF, é que se configurar o WCF para autenticar o cliente através de usuário e senha, ainda assim será necessário utilizar um certificado para garantir a segurança.

Configuração

O ASMX possibilita que algumas configurações sejam feitas de forma declarativa, ou seja, aquela que é realizada através do arquivo Web.config. Entre essas configurações, temos a possibilidade de definir as SoapExtesions, página de ajuda/WSDL customizada, os protocolos que podem ser utilizados para acessar o serviço (HttpSoap, HttpPost e HttpGet) e mais algumas outras.

No WCF, a seção de configurações são extremamente ricas. Grande parte de tudo que utilizamos no WCF pode ser configurado a partir do arquivo de configuração. Segurança, transações, know types, behaviors, bindings, endpoints, contratos, etc. O arquivo de configuração é tão rico que as vezes chega a ser complexo. A Microsoft melhorou isso no WCF 4.0, como já foi discutido neste artigo. Aqui não há muito o que se comparar, já que grande parte do que temos no WCF não existe nativamente no ASMX. Devido a isso, muitos desenvolvedores experientes na construção de serviço utilizando o ASMX, sofrem bastante quando passam a usar o WCF.

Compatibilidade com o ASP.NET

Dentro de métodos criados no ASMX, você pode tranquilamente acessar os objetos nativos do ASP.NET, como caching, Session, Application, Cookies, etc. Você pode utilizar esses repositórios para manter as informações de acordo com o contexto. Todas essas informações são disponibilizadas através da propriedade estática Current da classe HttpContext.

A configuração padrão do WCF não permite você acessar essas informações. Na verdade, isso não é uma boa prática. Se fizer uso dessas classes dentro do seu serviço, ele ficará dependente do protocolo HTTP. Se quiser expor o mesmo serviço através de TCP ou MSMQ, essas informações não terão nenhuma utilidade. De qualquer forma, se quiser manter a compatibilidade e continuar utilizando os mesmos recursos, então você deverá explicitamente habilitá-los. Para fazer isso, você deve decorar a classe que representa o serviço com o atributo AspNetCompatibilityRequirementsAttribute, definindo a propriedade RequirementsMode como Required.

Dependendo do tempo de vida e do escopo que deseja manter alguma informação, você pode recorrer a técnicas nativas do WCF, como o compartilhamento de estado, utilizando a interface IExtension<T>, como é abordado no final deste artigo.

Interoperabilidade

Há várias especificações que foram definidas por grandes players do mercado, que regem a estrutura do envelope SOAP para suportar alguma funcionalidade. Essas especificações são conhecidas como WS-* e estão divididas em várias categorias, sendo Messaging, Security, Reliable Messaging, Transaction, Metadata, entre outros. Cada uma das empresas utiliza essas especificações e as implementam em sua plataforma. Como todos seguem (teoricamente) as mesmas especificações, haverá interoperabilidade entre serviços construídos em plataformas diferentes.

O ASMX não possui nativamente suporte a elas. A Microsoft criou um Add-on para o Visual Studio .NET, chamado WSE – Web Services Enhancements, que atualmente está na versão 3.0. Ao instalá-lo, além de várias ferramentas que ele adiciona na IDE para auxiliar na configuração destes protocolos, adiciona o runtime necessário para fazer tudo isso funcionar. É importante dizer que o WCF consegue também interoperar com o WSE, já que ambos implementam os mesmos padrões.

Como esses padrões já foram implementados nativamente no WCF, não exige nenhum complemento. Todas as especificações são configuradas através do binding, podendo inclusive efetuar essa configuração através do arquivo Web.config. Antes de habilitar ou desabilitar essas funcionalidades, é importante que se tenha o devido conhecimento, para evitar qualquer problema ao até mesmo criar alguma vulnerabilidade. 

Ainda falando sobre interoperabilidade, a Microsoft se preocupou em manter os investimentos feitos com o ASMX. O WCF consegue facilmente conversar com serviços construídos em ASMX em ambos os lados, ou seja, você pode construir um serviço ASMX e consumí-lo com a infraestrutura do WCF, bem como pode construir um serviço escrito em WCF e consumí-lo com a infraestrutura do ASMX. Na verdade, quando você for criar um novo serviço, opte sempre pelo WCF. Você somente teria essa interoperabilidade entre essas tecnologias, quando desejar substituir o ASMX pelo WCF em serviços que já estão rodando, e com vários clientes dependendo do mesmo.

Internals

As requisições para arquivos *.asmx são direcionadas pelo IIS para o ISAPI do ASP.NET (aspnet_isapi.dll). Em um determinado momento, o ISAPI direciona a requisição do pedido para o código gerenciado. As requisições para estes arquivos tem como alvo um dos handlers WebServiceHandlerFactory ou ScriptHandlerFactory. A primeira delas, se baseando no arquivo requisitado, construirá dinamicamente a classe que representa o serviço. Já a classe ScriptHandlerFactory construirá o handler baseando-se em uma requisição REST/AJAX.

O WCF também utiliza o pipeline do ASP.NET, e quando a requisição é entregue pelo IIS para o ASP.NET, ele verifica se trata-se de uma requisição para um arquivo *.svc. Caso positivo, o handler responsável pelo processamento do pedido é o HttpHandler (System.ServiceModel.Activation). Internamente o WCF faz o processamento síncrono da operação, não liberando a thread do ASP.NET, que ficará bloqueada até a finalização da operação. Para entender melhor esse problema e também como melhorá-lo, você pode recorrer a este artigo.

Deployment

Assim como qualquer aplicativo .NET, basta mover os serviços para o IIS remoto e tudo já funciona. Obviamente que você deverá se certificar que você tenha a mesma versão do .NET Framework (isso inclui o Service Packs) instalada no servidor. É importante dizer que ambas tecnologias necessitam de um diretório virtual devidamente criado no IIS, com as permissões também configuradas. Apenas atente-se ao WCF, que tem um pequeno bug quando você opta pela pré-compilação do projeto.

Cliente

Dentro do Visual Studio .NET você tem duas opções para referenciar um serviço: “Add Web Reference” e “Add Service Reference”. Podemos dizer que com a primeira opção, você deve utilizar quando for referenciar um serviço ASMX em uma aplicação; já com a segunda opção, você deve utilizar quando você for referenciar um serviço WCF em uma aplicação. Quando fizer isso, a IDE criará o proxy utilizando a API da tecnologia correspondente.

Isso não quer dizer que você precisa seguir sempre esta regra. Você pode referenciar um serviço ASMX na opção “Add Service Reference”. Ao fazer isso, toda a estrutura do lado do cliente, será criada utilizando a API do WCF, ou seja, o proxy será baseado na classe ClientBase<TChannel> ao invés da SoapHttpClientProtocol, e toda a configuração do endpoint do serviço será colocada no arquivo Web.config. O WCF criou um binding chamado BasicHttpBinding para a interoperabilidade com o ASMX, que você pode utilizar para interagir com o serviço criado através do ASMX, sem maiores problemas.

Conclusão: Como vimos neste artigo, há vários detalhes que precisamos nos atentar na construção ou migração de um serviço escrito em ASMX para WCF. Para efeito de compatibilidade, algumas funcionalidades continuam trabalhando da mesma forma, enquanto para outras há um jeito diferente de fazer, e que na maioria das vezes, acaba sendo mais flexível. O artigo também não aborda detalhadamente cada uma das funcionalidades do WCF. Se desejar construir um serviço utilizando WCF, ou se quiser entender mais detalhadamente cada uma das funcionalidades abordadas aqui, pode consultar este artigo, que no final dele, há uma relação com os principais artigos que falam sobre essas funcionalidades.

Validando a Identidade do Serviço

O WCF fornece várias possibilidades para autenticar o cliente. Isso permitirá ao serviço somente possibilitar o acesso ao mesmo, se a autenticação do cliente for realizada com sucesso, independente do tipo de credencial e de como isso é avaliado. É muito comum quando falamos de autenticação, em pensar que somente isso acontecerá do lado do serviço.

Da mesma forma que o serviço precisa validar o cliente para conceder o acesso, o cliente também precisa validar o serviço, para saber se ele está enviando a mensagem para o local correto. Felizmente o WCF possibilita a autenticação mútua, ou seja, antes de efetivamente enviar a mensagem para o serviço, o cliente primeiramente fará algumas validações para determinar se o serviço para qual ele enviará a mensagem, é realmente quem ele diz ser.

Depois que o cliente inicia a comunicação com um endpoint e depois que o serviço o autentica, ele irá comparar a identidade do endpoint remoto com a identidade fornecida para o cliente durante a referência do mesmo, através do documento WSDL. Se os valores forem iguais, então o cliente assume que é o serviço correto. Isso irá proteger o cliente de possíveis ataques de phishing, evitando que o mesmo seja redirecionado para endpoints maliciosos.

Essa validação ocorre de forma transparente e dependerá de qual tipo de identidade é fornecida pelo serviço. Atualmente temos cinco possibilidades: Domain Name System (DNS), Certificate, Certificate Reference, RSA, UPN (User Principal Name) e SPN (Service Principal Name). O uso de cada um deles dependerá do cenário e dos requerimentos de segurança exigidos. Para saber mais detalhes sobre cada um deles, consulte este artigo.

Se você quiser interceptar esse processo, você poderá criar um “autenticador” customizado. Isso nos permitirá customizar como essa validação deverá ser realizada, ficando sob nossa responsabilidade determinar se o serviço para qual o cliente está tentando enviar a mensagem é válido ou não.

Para que isso seja possível, é necessário criarmos uma classe que herde da classe abstrata IdentityVerifier (namespace System.ServiceModel.Security) e implementar os métodos TryGetIdentity e CheckAccess. O primeiro deles, recebe um parâmetro de output do tipo EnpointIdentity que será populado, com um objeto que representa a identidade remota do serviço, retornando um valor boleano indicando se a identidade foi ou não criada. O segundo método, CheckAccess, é onde devemos efetivamente validar se a identidade do serviço é realmente válida. Esse método também retorna um valor boleano, só que neste caso, indicando se a identidade é válida ou não, de acordo com a regra que vamos customizar. O código abaixo ilustra a implementação desta classe:

public class ValidadorDeIdentidadeDoServico : IdentityVerifier
{
    public override bool CheckAccess(EndpointIdentity identity, AuthorizationContext authContext)
    {
        foreach (var set in authContext.ClaimSets)
        {
            foreach (var claim in set)
            {
                Console.WriteLine(claim.ClaimType);
                Console.WriteLine(“t{0}”, claim.Resource);
            }
        }

        Console.WriteLine();
        return true;
    }

    public override bool TryGetIdentity(EndpointAddress reference,
        out EndpointIdentity identity)
    {
        return IdentityVerifier.CreateDefault().TryGetIdentity(reference, out identity);
    }
}

Depois dessa classe criada, precisamos acoplá-la na execução do proxy. Mas para isso, iremos precisar de um binding customizado. Através do método abaixo vamos efetuar essa configuração. Note que ele atribui a instância da classe ValidadorDeIdentidadeDoServico na propriedade IdentityVerifier, que será responsável pela validação do serviço.

public static Binding ConfigurarBinding()
{
    WSHttpBinding binding = new WSHttpBinding(SecurityMode.Message);
    binding.Security.Message.ClientCredentialType = MessageCredentialType.UserName;
    binding.Security.Message.EstablishSecurityContext = true;

    BindingElementCollection elements = binding.CreateBindingElements();
    SymmetricSecurityBindingElement ssbe =
        (SymmetricSecurityBindingElement)elements.Find<SecurityBindingElement>();

    ssbe.LocalClientSettings.IdentityVerifier = new ValidadorDeIdentidadeDoServico();
    return new CustomBinding(elements);
}

Para finalizar, temos que configurar o proxy para, ao invés de utilizar a configuração padrão do arquivo de configuração, usar o binding customizado, onde definimos a classe que será responsável por efetuar a validação do serviço. O código abaixo ilustra como fazemos a vinculação do binding ao proxy:

using (ContratoClient p =
    new ContratoClient(ConfigurarBinding(),
    new EndpointAddress(“http://localhost:2334/srv&#8221;)))
{
    //…
}

Strong Name Bypass

Um dos passos necessários durante a carga do Assembly é análise da assinatura do Strong Name. Independentemente do Code Access Security, essa análise sempre é realizada e muitas aplicações pagam esse preço sem ao menos utilizá-lo. A partir do SP1 do .NET Framework 3.5, a Microsoft incluiu uma funcionalidade chamada de Strong Name Bypass, não mais fazendo essa verificação e, consequentemente, tendo um ganho de performance durante a sua inicialização.

Essa análise não será mais realizada para Assemblies que são carregados em um ambiente full-trusted. Obviamente que essa análise acontecerá quando, para computar a segurança, voce levar em consideração o Strong Name. Se, por algum motivo, voce quiser reabilitar a verificação, então pode recorrer ao elemento bypassTrustedAppStrongNames, definindo o atributo enabled para false, assim como é mostrado abaixo, utilizando o arquivo de configuração:

<configuration>
    <runtime>
        <bypassTrustedAppStrongNames enabled=”false”/>
    </runtime>
</configuration>

Melhorando a performance no ThreadPool

Como já sabemos, a classe ThreadPool disponibiliza um repositório de threads que podemos utilizar em nossas aplicações, delegando a ela uma tarefa para ser executada através de um delegate. Para isso, é muito comum utilizarmos o método QueueUserWorkItem para enfileirar uma nova tarefa. O exemplo abaixo ilustra a sua utilização em uma aplicação qualquer:

ThreadPool.QueueUserWorkItem(state =>
{
    Console.WriteLine(new StreamReader(@”C:TempArquivo.txt”).ReadToEnd());
});

Quando utilizamos o método QueueUserWorkItem, antes de enfileirar a tarefa a ser executada, ele captura todas as permissões que foram concedidas e, antes de executá-la, essas permissões são aplicadas a thread que está associada à tarefa. Com isso, se a tarefa que voce está tentando executar de forma assíncrona exigir alguma permissão e ela não foi concedida, uma exceção do tipo SecurityException será disparada. Podemos notar esse comportamento com o seguinte código:

new FileIOPermission(FileIOPermissionAccess.Read, @”C:TempArquivo.txt”).Deny();

ThreadPool.QueueUserWorkItem(state =>
{
    Console.WriteLine(new StreamReader(@”C:TempArquivo.txt”).ReadToEnd());
});

No código acima, a aplicação não possui permissão de acesso ao arquivo, e ao executar o método QueueUserWorkItem, a tarefa vinculada também não conseguirá executar a leitura do arquivo, disparando uma exceção. Por menor que seja, utilizar este método sempre tem o overhead para a cópia das permissões atuais e associá-las a thread responsável pela tarefa, para que ela execute no mesmo contexto de segurança.

É justamente neste ponto que entra em cena o método UnsafeQueueUserWorkItem, também da classe ThreadPool. Ao contrário do método QueueUserWorkItem, ele não propaga as permissões para a worker thread, diminuindo o trabalho a ser realizado quando voce enfileira uma nova tarefa ao ThreadPool. A utilização em relação ao que vimos acima muda ligeiramente:

new FileIOPermission(FileIOPermissionAccess.Read, @”C:TempArquivo.txt”).Deny();

ThreadPool.UnsafeQueueUserWorkItem(state =>
{
    Console.WriteLine(new StreamReader(@”C:TempArquivo.txt”).ReadToEnd());
}, null);

Como as permissões não são propagadas e avaliadas com o método UnsafeQueueUserWorkItem, mesmo que a aplicação não tenha as devidas permissões, a execução da tarefa ocorre sem maiores problemas.

É importante dizer aqui que a performance é melhorada mas, em contrapartida, a segurança fica vulnerável. Utilizar este tipo de código pode abrir portas para algum código malicioso, que pode utilizá-lo para elevar os privilégios de uma aplicação e, consequentemente, executar tarefas que atualmente ela não tem permissão. Opte sempre por utilizar esta técnica em uma tarefa em que voce não dependerá de nenhum recurso do sistema operacional (sistema de arquivos, registry, SQL Server, etc.), utilizando em conjunto com tarefas que dependam exclusivamente do processador, como é o caso de cálculos complexos, geração de algum conteúdo, etc., melhorando consideravelmente a performance na execução, sem abrir potenciais problemas de segurança.

DBAuthorization – Parte 10 – Conclusão

Finalmente chegamos na conclusão desta série. A partir deste momento você tem todas as políticas de acesso as páginas dentro do banco de dados, com uma arquitetura flexível e que pode ser estendida para ser suportada por outros repositórios.

Uma vez que o provider foi criado, sempre que nós precisamos das informações, seja para aplicar a autorização durante a execução ou pelas telas de gerenciamento das informações, sempre utilizamos o provider para isso (classe DBAuthorization), o que torna a aplicação e a API totalmente independente de qual fonte de dados estamos utilizando para armazenar essas informações.

É importante dizer também que, assim como a UrlAuthorization, a DBAuthorization avalia as permissões de forma sequencial, ou seja, a ordem em que elas aparecerem cadastradas no banco de dados será a ordem de avaliação durante a execução. Neste momento não temos nenhuma forma de reordenação dos registros, mas isso pode ser facilmente implementado. Um boa prática aqui é sempre você ir cadastrando em uma ordem coerente, ou seja, negue o acesso anônimo para um diretório como um todo mas, dentro dele, customize o acesso à algumas seções de acordo com os papéis.

Outra semelhança que o DBAuthorizationModule possui em relação ao UrlAuthorizationModule é que faz uso da propriedade User da classe HttpContext. Essa propriedade retorna a instância de uma classe que implementa a interface IPrincipal e, independentemente do tipo de autenticação que está utilizando (Forms (GenericPrincipal) ou Windows (WindowsPrincipal)), ele sempre irá trabalhar de forma genérica. Utilizará a propriedade IIdentity para determinar o nome do usuário e o método IsInRole para avaliar se o usuário faz ou não parte de um determinado papel/grupo.

Para efeitos de centralização e reusabilidade da API, você pode instalar essa DLL dentro do GAC (Global Assembly Cache) e registrar a seção “dbAuthorization” no arquivo machine.config. Esta técnica evitará com que todas as aplicações que desejam fazer uso deste recurso, não precisem explicitamente fazer o registro.

Para finalizar, todo esse projeto não foi implementando em um ambiente real. Os testes foram locais e, se desejar colocá-lo em um ambiente de produção, chamo a atenção para que você refaça os testes, analisando todas as possibilidades necessárias para garantir o bom funcionamento dele.

Quero também agradecer imensamente ao Luis Abreu, expert em tecnologias Microsoft, que revisou e me deu dicas extremamente importantes para ajudar na elaboração desta série. O código para download está no seguinte endereço: http://www.israelaece.com/BlogEngine.Web/file.axd?file=2009%2f4%2fDBAuthorization.zip.