Israel Aece

Software Developer

Explorando Segurança do ASP.NET - AJAX

A onda do momento é o Framework Atlas para processamento de códigos server-side, sem a necessidade da reconstrução completa da página. Isso torna a aplicação Web muito parecida com aplicações Windows, deixando-a muito mais interativa.

Devido a isso, nas últimas versões do Atlas (ainda em versões não Release), a Microsoft implementou métodos dentro de um determinado objeto para trabalhar diretamente com a API de Membership, sem a necessidade de um refresh e a reconstrução da página toda. O objeto que disponibiliza estes serviços chama-se Sys.Services.AuthenticationService e é um objeto desenvolvido em Javascript. Este, por sua vez, têm três métodos, os quais analisaremos as suas utilidades na tabela abaixo:

Método Descrição
validateUser

Este método recebe dois parâmetros: login e senha, e se encarrega de validar junto ao Membership padrão que encontra-se configurado para a aplicação, se as credenciais informadas são ou não válidas, retornando um valor booleano com essa indicação.

login

Este método possui basicamente a mesma funcionalidade que o validateUser, mas com uma diferença: além de validar as credenciais e estas estiverem válidas, ele já devolve para o browser o cookie de autenticação especificado no FormsAuthentication. É importante dizer que este método também recebe um parâmetro booleano que indica se o cookie será ou não persistente.

logout Efetua a saída do usuário, limpando o cookie.

Quando adicionamos um projeto ASP.NET Atlas no Visual Studio .NET 2005, a autenticação via Altas vem desabilitada no Web.Config e é necessário habilitarmos para fazer o uso do objeto de autenticação. Para se certificar disso, é necessário que o arquivo Web.Config da aplicação ASP.NET Atlas contenha as seguintes entradas:

<sectionGroup 
    name="microsoft.web" 
    type="Microsoft.Web.Configuration.MicrosoftWebSectionGroup">
    <!-- outras seções -->
    <section 
	name="webServices" 
	type="Microsoft.Web.Configuration.WebServicesSection" 
	requirePermission="false" />
    <section 
	name="authenticationService" 
	type="Microsoft.Web.Configuration.AuthenticationServiceSection" 
	requirePermission="false" />
</sectionGroup>

<!-- 
    
    Certifique-se de que o Provider padrão esteja configurado
    corretamente e apontando para uma base de dados válida.

    As seções de configurações de SqlMembershipProvider
    foram suprimidas para poupar espaço, mas não muda nada
    em relação aos exemplos anteriores.
    
-->

<webServices enableBrowserAccess="true" />
<authenticationService enabled="true" />

Notem que há um elemento chamado webServices que neste cenário, é responsável por efetuar a autenticação. Na página que fará a autenticação via Atlas você notará que não se tem código em VB.NET ou C#, tudo será escrito utilizando Javascript. Acredito que, como já aconteceu com alguns controles, nas próximas versões isso já esteja encapsulado em controles drag-and-drop. Para iniciarmos o entendimento da estrutura cliente, a página irá conter um controle do tipo ScriptManager, o qual tem a finalidade de expor (código cliente) grande parte das funcionalidades e controles do Atlas.

É através do controle ScriptManager que temos acesso ao objeto Sys.Services.AuthenticationService. Agora que já temos acesso à ele, resta codificar para efetuarmos o login do usuário. Como qualquer outro sistema de Login, é necessário termos dois controles TextBox e um Button. Teremos também Label para exibirmos a mensagem de autenticado/não autenticado para o usuário saber se foi ou não autenticado com sucesso. O código é mostrado abaixo:

<html xmlns="http://www.w3.org/1999/xhtml">
    <head id="Head1" runat="server">
        <atlas:ScriptManager 
            ID="scriptManager" 
            runat="server" 
            EnableScriptComponents="false" >
        </atlas:ScriptManager>
    </head>
<body>
    <form id="form1" runat="server">
        <span id="TextoLogado" 
            style="visibility:hidden;color:Green" 
            visible="false">
            <b>Logado.</b>
        </span> 
        <span 
            id="TextoNaoLogado" 
            style="visibility:visible;color:Red">
            <b>Não logado.</b>
        </span>
        <br /><br />        
        
        Login: <input type="text" id="txtLogin" />
        <br />
        Senha: <input type="password" id="txtSenha" />
        <br /><br />
        <button id="btnEnviar" onclick="OnSubmitLogin()">Efetuar Login</button>

        <script type="text/javascript" language="JavaScript">
            var login = document.getElementById('txtLogin');
            var senha = document.getElementById('txtSenha');
            var lblLogado = document.getElementById('TextoLogado');
            var lblNaoLogado = document.getElementById('TextoNaoLogado');
            var buttonLoginLogout = document.getElementById('btnEnviar');            
                
            function OnSubmitLogin() { 
                Sys.Services.AuthenticationService.login(
                    login.value, 
                    senha.value, 
                    false, 
                    OnLoginComplete); 
                    
                return false;
            }
            
            function OnLoginComplete(result) {                
                senha.value = '';
                if (result) {
                    login.value = '';
                    lblLogado.style.visibility = "visible";
                    lblNaoLogado.style.visibility = "hidden";  
                   
                    buttonLoginLogout.innerText = "Efetuar Logout";         
                    buttonLoginLogout.onclick = OnSubmitLogout;
                }
                else {
                    lblLogado.style.visibility = "hidden";
                    lblNaoLogado.style.visibility = "visible"; 
                }
            }
           
            function OnSubmitLogout() {  
                Sys.Services.AuthenticationService.logout(OnLogoutComplete); 
                return false;
            }
           
            function OnLogoutComplete(result) {
                lblLogado.style.visibility = "hidden";
                lblNaoLogado.style.visibility = "visible";  
                buttonLoginLogout.innerText = "Efetuar Login";  
                buttonLoginLogout.onclick = OnSubmitLogin;
            }
        </script>
   </form>
</body>
</html>

No exemplo acima utilizamos o método login para autenticar o usuário e, via Javascript, fazemos as manipulações necessárias para assim que o método login retornar o valor (com sucesso ou não), o layout é ajustado, exibindo uma mensagem de que o usuário foi autenticado com sucesso.

Informamos ao método login, além do login e senha do usuário (recuperado dos TextBoxes), um parâmetro booleano que indica se o cookie vai ou não ser persistente. O último parâmetro é a função de callback, a qual será disparada assim que o método de login finalizar o processamento, já que o mesmo é processado assincronamente. Já para o método logout (que irá chamar o método Logout da classe FormsAuthentication), você passa apenas o método que será disparado assim que o método for finalizado, e é neste método que você fará a manipulação do layout para dar um feedback ao usuário que, no caso acima, apenas alteramos o valor do Label para "Não Logado.".

Este exemplo foi baseado na versão de 06/04/2006 do Atlas e os códigos mostrados acima são baseados no exemplo exibido nos QuickStarts do Atlas. É possível que nas versões futuras do Atlas este objeto de autenticação client-side possa mudar.

Explorando Segurança do ASP.NET - Arquitetura

O ASP.NET 2.0 inclui uma porção de novos serviços de persistência de dados em um banco de dados. Essa nova arquitetura é chamada de Provider Model e nos dá uma enorme flexibilidade, onde podemos trocar a fonte de dados/persistência e a aplicação continua trabalhando normalmente. Este provider é um módulo do software que estamos desenvolvendo que fornece uma interface genérica para uma fonte de dados, onde abstraem a base de dados. Além disso ser flexível, a qualquer momento podemos trocar a base de dados, essa arquitetura também é extensível, ou seja, se mais tarde quisermos customizar algo, podemos fazer isso sem maiores problemas.

A idéia dentro deste padrão é ter uma classe abstrata, onde dentro dela teremos métodos e propriedades que devem ser implementados nas classes concretas e, através de configurações no arquivo Web.Config, definimos a classe concreta que iremos utilizar para a aplicação. A questão é que essa classe concreta é instanciada em runtime, pois o ASP.NET recupera isso do arquivo de configuração e se encarrega de criar a instância correta da classe para uma determinada funcionalidade.

Essa arquitetura já não é lá muito nova. Se analisarmos o ASP.NET Fóruns ou até mesmo o PetShop 3.0, veremos que eles utilizam uma técnica bem parecida, onde fazem o uso dos padrões Abstract Factory e Factory Method (padrões Criacionais) para garantir essa genericidade. Isso faz com que devemos ter uma classe abstrata por trás a qual as classes concretam as implementam e o runtime se encarrega de criar a instância correta da classe concreta baseando-se nas configurações do arquivo Web.Config.

Essa arquitetura é usada extensivamente dentro do .NET Framework 2.0 (ASP.NET), onde temos classes abstratas para cada situação diferente e, para um determinado repositório de dados, uma classe concreta já implementada. Através da tabela abaixo veremos o nome da funcionalidade, a classe base e as classes concretas que se enquandram dentro do escopo do artigo:

Funcionalidade Classe Base Classes Concretas Descrição
Membership MembershipProvider SqlMembershipProvider
ActiveDirectoryMembershipProvider
Responsável por gerenciar os usuários de uma aplicação ASP.NET.
Roles RoleProvider SqlRoleProvider
WindowsTokenRoleProvider
AuthorizationStoreRoleProvider
Utilizado para gerir os papéis dos usuários dentro da aplicação ASP.NET.


Reparem que para uma determinada classe abstrata, como por exemplo MembershipProvider, já temos, por padrão, algumas classes que a Microsoft implementou para já utilizarmos a funcionalidade. Um exemplo disso é a classe SqlMembershipProvider, a qual utiliza uma base de dados SQL Server 2000 ou superior para disponibilizar o recurso. Como foi dito acima, não se restringe somente à isso. Temos classes para gerencimento de estado, Web Parts, Site Map, Profile, etc., que não foram citados/explicados por não fazerem parte do escopo deste artigo. Se no futuro precisarmos customizar alguma das funcionalidades da tabela acima para uma base de dados, como por exemplo Oracle, basta criarmos uma classe herdando de MembershipProvider ou RoleProvider e implementar os métodos e propriedades exclusivamente para aquela base de dados. Finalmente, para ilustrar essa arquitetura, é mostrado através da figura abaixo o design das classes, já com as devidas denotações de herança entre elas:

Figura 1 - Design das classes utilizando Provider Model.

Além das classes que vimos acima, temos ainda duas classes estáticas (compartilhadas) que são, também, parte das principais. São estas classes que são expostas para nós, desenvolvedores, utilizarmos e fazermos a chamada aos métodos e propriedades de forma genérica. Essas classes são: Membership e Roles, as quais estão contidas dentro do Namespace System.Web.Security. No caso da classe Membership, existe um membro interno chamado s_Provider do tipo MembershipProvider (a classe base para as classes concretas de Membership, como por exemplo o SqlMembershipProvider), o qual receberá a instância da classe concreta. Essa inicialização acontece quando um método interno chamado Initialize é executado. Ele se encarrega de extrair os providers do arquivo Web.Config e instanciá-los para que, quando chamarmos os métodos e propriedades, já sejam efetivamente os métodos e propriedades da classe concreta que queremos utilizar. O funcionamento é também semelhante para a classe Roles.

Por questões de infraestrutura, utilizaremos no decorrer deste artigo o banco de dados SQL Server 2000. Mas então como prepará-lo para fazer o uso desta funcionalidade? Pois bem, para que o mesmo possa ser utilizado para isso, é necessário criarmos dentro do banco de dados a infraestrutura (Tabelas, Índices, Stored Procedures e Triggers) necessária para a utilização do recurso, que no caso serão Membership e Roles.

Quando instalamos no .NET Framework 2.0, em seu diretório %WinDir%\Microsoft.NET\Framework\v2.0.50727 é instalado um utilitário chamado aspnet_regsql.exe. Este utilitário, dados alguns parâmetros, faz todo o trabalho da criação da infraestrutura dentro de um determinado banco de dados SQL Server. Veremos na tabela abaixo os parâmetros que ele aceita:

Parâmetro Descrição
-? Exibe o Help do utilitário.
-W Roda o utilitário em modo Wizard. Esse é o padrão caso nenhum outro parâmetro seja informado. Com isso, você terá um wizard (que é mostrado abaixo) para acompanhá-lo durante o processo.

-C Especifica a ConnectionString para o servidor onde o banco de dados SQL Server está instalado. Se desejar, você pode informar isso separadamente (mostrado mais abaixo).
-S Nome do servidor onde o SQL Server está.
-U Login de acesso dentro do SQL Server (é necessário informar o password ou Integrated Security).
-P Password para o Login especificado através do parâmetro -U.
-E Autentica o usuário com as credenciais do usuário corrente logado no Windows.
-sqlexportonly Usado para criar um arquivo de script para adicionar ou remover tais funcionalidades.
-A all|m|r|p|c|w Adiciona a infraestrutura dentro da base de dados para uma determinada funcionalidade. Além do -A, ainda há o parâmetro complementar onde você deve informar qual será a funcionalidade que deseja adicionar:

all - Todas
m - Membership
r - Role management
p - Profile
c - Web Parts personalization
w - Web events
-R all|m|r|p|c|w Remove a infraestrutura dentro da base de dados para uma determinada funcionalidade. Além do -R, ainda há o parâmetro complementar onde você deve informar qual será a funcionalidade que deseja remover:

all - Todas
m - Membership
r - Role management
p - Profile
c - Web Parts personalization
w - Web events

Abaixo é mostrado (através do prompt de comando do Visual Studio .NET 2005) alguns exemplos do uso do utilitário aspnet_regsql.exe:

C:\aspnet_regsql.exe -S localhost -U NomeUsuario -P P@$$w0rd -d BancoDados -A mrpw
C:\aspnet_regsql.exe -S localhost -E -d BancoDados -A all
C:\aspnet_regsql.exe -S localhost -E -d BancoDados -R all

Na primeira linha adicionamos as funcionalidades de Membership, Roles, Profile e WebEvents no banco de dados chamado "BancoDados". Já na segunda opção adicionamos todas as funcionalidades, só que agora utilizando as credenciais do Windows. E, por último, estamos removendo toda a infraestrutura da base de dados. Quando criamos uma destas funcionalidades dentro da base de dados ele inclui uma porção de Tabelas, Stored Procedures, Triggers para que a mesma seja atendida. Se analisarmos o design da base de dados depois disso, veremos a seguinte estrutura:

Figura 2 - Design da base de dados para suportar as funcionalidades de Membership e Roles.

O Arquivo ASPNETDB.MDF

Quando utilizamos Membership ou qualquer uma destas funcionalidades, se não especificarmos um provider e você não tiver um banco de dados pré-definido para o uso das mesmas, o ASP.NET cria automaticamente dentro do diretório App_Data dentro da aplicação um banco de dados chamado ASPNETDB.MDF, que faz uso do SQL Server 2005 Express Edition. Essa criação se dá quando iniciamos o WSAT - Web Site Administration Tool, onde depois de iniciarmos o provider, o arquivo MDF é criado dentro da pasta App_Data.

Com isso não seria necessário você criar a infraestrutura em uma base de dados assim como mostramos acima, pois o arquivo ASPNETDB.MDF já terá todo o schema necessário para as funcionalidades. Se o volume de usuários não é tão alto e o sistema pouco complexo, ter o ASPNETDB.MDF já resolve a necessidade. Mas quando o nível de usuários aumenta e de alguma forma você precisa fazer o relacionamento da tabela de Usuários com outras tabelas do sistema, é mais interessante ter isso unificado, ou seja, dentro de uma única base de dados.

Outputcache - CacheProfile

Estou trabalhando em um projeto ASP.NET 2.0 e, como algumas páginas estavam requerendo o uso de Outputcache, para cada uma dessas páginas estava ajustando o mesmo valor de segundos no atributo Duration e também especificando algumas outras propriedades desta mesma diretiva, customizando assim, o cache da forma que necessitava.

Mas em um certo momento me perguntei: e se esses valores e condições mudarem? Sim, terei que ir a cada página e especificar tudo novamente. Foi nesse momento que recorri a documentação do .NET Framework e encontrei o elemento outputCacheProfiles que é especificado dentro do arquivo Web.Config.

Com isso, ao invés de especificar os mesmos valores em cada página ASPX, eu criei uma "profile" dentro do Web.Config e passo a utilizar este "profile" nas páginas ASPX. Um exemplo do uso é mostrado abaixo:

[ Web.Config ]
<outputCacheSettings>
  <outputCacheProfiles>
    <add name = "PaginasEmCache"
      varyByParam = "TituloID"
      enabled = "true"
      duration = "180" />
  </outputCacheProfiles>
</outputCacheSettings>

[ *.ASPX ]
<%@ OutputCache CacheProfile="PaginasEmCache" %>

Recuperando a linha do GridView

Muitas pessoas me perguntam como acessar o conteúdo de uma linha do GridView dentro do evento RowCommand (sim, elas NÃO ESTÃO fazendo o uso do DataKeys). Pois bem, nas versões 1.x do ASP.NET, o evento ItemCommand do DataGrid tinha um argumento do tipo DataGridCommandEventArgs e, dentro deste, uma propriedade chamada Item que retorna a instancia da linha corrente.

Isso não existe mais no GridView e, se ainda desejar acessar os dados da linha clicada no evento RowCommand do GridView, terá que proceder da seguinte forma: no evento RowCreated do GridView, terá que definir à propriedade CommandArgument do controle LinkButton (Button, ImageButton, etc) responsável por disparar o comando, a propriedade RowIndex que vem como parametro para este evento. Esta propriedade retorna um valor inteiro contendo o índice da linha e, como o evento RowCommand te fornece uma propriedade chamada CommandArgument, conseguirá recuperar o índice da linha e, consequentemente, acessar os valores da mesma.

Para ilustar o processo, o código (em C#) é mostrado abaixo:

private void GridView1_RowCommand(Object sender, GridViewCommandEventArgs e)
{
   if(e.CommandName=="Add")
   {
      int index = Convert.ToInt32(e.CommandArgument);
      GridViewRow row = GridView1.Rows[index];
      Response.Write(row.Cells[2].Text);
   }
}

private void GridView1_RowCreated(Object sender, GridViewRowEventArgs e)
{
   if(e.Row.RowType == DataControlRowType.DataRow)
   {
      LinkButton addButton = (LinkButton)e.Row.Cells[0].Controls[0];
      addButton.CommandArgument = e.Row.RowIndex.ToString();
   }
}

ListItem

Um dos grandes problemas (bug?) que temos nas versões 1.x do ASP.NET é que os atributos do objeto ListItem (usado para preencher controles como DropDownLists, ListBoxes, etc.) não são renderizados, logo, se quisésemos definir uma cor de background em um item do DropDownList, não era possível, a menos que fizessemos algo como o Scott Mitchell mostra neste artigo.

A boa notícia é que isso foi resolvido na versão 2.0 do ASP.NET e agora podemos fazer o que já cansamos de tentar nas versões anteriores:

foreach (ListItem item in this.DropDownList1.Items)
{
    item.Attributes.Add("style", "background-color: Red");
}

Atributo debug

O ScottG fala neste post sobre o atributo debug do elemento compilation do arquivo Web.Config e me fez lembrar o meu primeiro projeto em ASP.NET. A performance estava muito baixa e depois de uma checagem antes de mandar ao servidor de produção, alterei o atributo, definindo-o para false. A performance aumenta bastante, já que muitas coisas que devem ser usadas somente em tempo de desenvolvimento, deixam de ser utilizadas. Só vale lembrar que isso não é uma característica exclusiva do ASP.NET 2.0.

Uma das outras técnicas interessantes, esta somente para ASP.NET 2.0, é definir no arquivo machine.config do servidor de aplicações, o atributo retail do elemento deployment para true, para evitar que, pessoas despercebidas façam o deployment de aplicações sem mudar/definir o atributo debug para false do arquivo Web.Config da respectiva aplicação.

Acessar dados do Arquivo do Upload

Muitas vezes, quando precisamos fazer um upload e, antes que o mesmo seja efetivamente salvo no disco, devemos aplicar algumas validações onde, serão estas validações que irão dizer se o arquivo é ou não um arquivo válido para a nossa necessidade e, consequentemente, salvar no disco.

O que muito gente não sabe (devido ao número de e-mails que recebo com esse, até então, problema) é que há a possibilidade de analisar o arquivo antes de salvá-lo. É possível graças a propriedade chamada InputStream da classe HttpPostedFile (acessível através do controle FileUpload). Com isso, podemos tranquilamente acessar o arquivo, ler e, se for um arquivo válido, o persistimos fisicamente. Para exemplificar, o código abaixo ilustra isso:

if (Page.IsValid && this.FileUpload1.HasFile){
     StreamReader sr =
          new StreamReader(this.FileUpload1.PostedFile.InputStream);
     //....
}

Obviamente que poupei o error-handling por questões de espaço. Como há um construtor na classe StreamReader que permite passarmos um Stream, podemos passar o Stream que vem quando fazemos o upload de um arquivo. Depois, se necessário, podemos utilizar o método SaveAs para salvar o arquivo fisicamente no disco, mas a idéia com este post, é mostrar que não é necessário salvarmos o arquivo fisicamente para análisá-lo.