Programação em Asp.Net - parte IX
Autor: Luís Abreu
Conteúdo: Introdução à programação em Asp.Net 2.0
Ferramentas: Visual Studio December CTP
O último artigo foi o primeiro de uma série de artigos relacionados com as novidades de segurança presentes na nova plataforma Asp.Net. Ao longo desse artigo falámos da nova API utilizada para efectuar a validação e criação de contas de utilizadores. Esta nova API vem colmatar uma das lacunas da primeira versão da plataforma introduzindo um conjunto de métodos que permitem criar, modificar, eliminar e validar utilizadores. Ao contrário do que se poderia pensar, esta nova API é muito flexível pois assenta no conceito de provider.
Este padrão, que será utilizado por todos os blocos que necessitem de armazenar informação(como iremos ver em artigos futuros), baseia-se numa abordagem dividida em camadas. Assim, a camada mais baixa é responsável por obter/persistir os dados utilizados pelas camadas superiores. A camada de topo, exposta através da nova API (conhecida por Membership API) , utiliza indirectamente essa camada. Isto deve-se ao facto da classe Membership recorrer a uma classe auxiliar que obtém a informação acerca do provider (que deve ser utilizado) a partir do ficheiro de configuração da aplicação (para os mais distraídos, estamos a falar do ficheiro web.config).
Como é possível depreender, a flexibilidade deste mecanismo é imensa. Se por acaso quisermos reaproveitar o esquema de dados antigo ou se precisarmos de aceder a outro tipo de base de dados só temos de desenvolver o nosso provider (um provider será sempre uma classe que é responsável por efectuar o acesso a uma fonte de dados, ou seja, estamos a falar da classe mais baixa a nível da hierarquia). Claro que existe um determinado contrato que tem de ser respeitado por todos os providers: é essa a função da classe MembershipProvider. Num artigo futuro iremos abordar a construção de um provider personalizado.
Para além da utilização de providers, vimos ainda como é que podemos utilizar a nova API em conjunto com os novos controlos por forma a executarmos várias operações associadas à segurança. Hoje vamos continuar o nosso estudo da segurança e vamos falar um pouco sobre os novos controlos que auxiliam o programador no desenvolvimento de operações relacionadas com as contas do utilizador.
Esquema básico utilizado pelos providers definidos
No último artigo falámos sobre a utilização da API mas não houve tempo para falarmos sobre o esquema de dados definido pelas bases de dados onde são armazenados os dados das contas do utilizador. A versão actual da plataforma (CTP de Dezembro) possui dois providers: um para Access (definido pela classe AcessMembershipProvider) e outro para Sql Server (representado pela classe SqlMembershipProvider). A utilização do provider de Access (configurado por defeito) não implica qualquer esforço por parte do programador. Por outro lado, a utilização do provider de Sql Server implica a configuração prévia através da execução do executável aspnet_regsql.
Qualquer que seja o provider escolhido, podemos partilhar a mesma base de dados por várias aplicações. Isto deve-se ao facto das tabelas criadas manterem os dados do utilizador associados ao nome da aplicação actual. Este nome é definido através da propriedade applicationName (definida através do elemento <add> no interior do elemento <providers>. Quando utilizamos o provider de Access é normal definirmos uma base de dados para cada aplicação. A imagem seguinte apresenta o esquema definido pelo provider de Access:

Como é possível observar, a tabela de topo é a tabela aspnet_Applications, que permite definir o nome da aplicação e uma (eventual) pequena descrição sobre esta. A definição de uma aplicação é essencial pois permite filtrar um conjunto de utilizadores (aspnet_Users) e papeis (aspnet_Roles) por aplicação. Se esta tabela não existisse não seria possível armazenar a informação sobre utilizadores de aplicações diferentes na mesma base de dados. Os dados associados à classe MembershipUser encontram-se dividos por duas tabelas: aspnet_Users e aspnet_Membership. A primeira tabela permite armazenar dados básicos associados ao utilizador, como por exemplo, o UserName e a data da última actividade. Para além disso, possui ainda um campo que indica se o utilizador actual é ou não anónimo. A tabela aspnet_Membership contém dados associadosà restante informação disponibilizada pela classe MembershipUser. Assim, podemos encontrar informação relacionada com o e-mail, palavra chave ( e respectivo formato), data de criação, data do último login, etc.
A tabela aspnet_UsersInRoles permite relacionar um utilizador com um eventual papel (algo que iremos falar num artigo futuro). A base de dados possui ainda tabelas relacionados com a definição de perfis (ou profiles - abordados num artigo futuro) e personalização de Web Parts. Neste último caso, existem duas categorias: dados armazenados por utilizador ou dados armazenados para todos os utilizadores. Finalmente temos ainda informação associada aos contadores que são utilizados para efeitos estatísticos do site.
O provider de Sql Server utiliza um esquema muito semelhante a este, como a figura seguinte demonstra:

Como este provider disponibiliza mais funcionalidades, possui alguns campos adicionais. Os campos adicionais definidos na tabela aspnet_Membership permitem controlar o número de logins falhados de forma a bloquear a conta do utilizador. Contudo, como é possível observar a partir da figura anterior, a grande maioria das tabelas são praticamente iguais às apresentadas pelo provider de Access.
Controlos de segurança
De forma a facilitar o trabalho dos programadores, a nova versão da plataforma fornece um conjunto de controlos que permite efectuar a maioria das acções associadas a contas de utilizadores. Se repararmos na nova Toolbox que acompanha o Visual Studio podemos verificar que existe uma categoria designada de Login que contém todos os controlos de segurança existentes na framework.
Controlo Login
Vamos iniciar o nosso estudo com o controlo designado de Login. Como o próprio nome indica, este controlo é utilizado para efectuar a validação das credenciais do utilizador. Por defeito, este controlo efecuta a validação através do provider (de Membership) definido pela aplicação. Por defeito, o controlo permite a introdução do nome de utilizador e da palavra chave. Como era de esperar, o controlo permite definir por completo o seu layout. Assim, podemos definir os estilos associados às caixas de texto, as legendas associadas às caixas de texto, etc. Podemos mesmo definir a orientação do controlo (ou seja, se desejamos gerar os elementos na vertical ou na horizontal).
Para além dos estilos, podemos ainda definir alguns aspectos relacionados com urls associado a este tipo de acção. Por exemplo, podemos definir um url associado à página de recuperação de palavra chave ou de ajuda ao utilizador. Se for necessário, podemos definir ainda um link para uma eventual página de criação de conta de utilizador (algo que é muito utilizado em foruns e sites de registo livre). Como é óbvio podemos também definir a página de navegação em caso de login efectuado com sucesso.
Este controlo possui um conjunto de eventos interessantes. Os eventos LoggingIn e LoggedIn estão associados ao evento de validação dos dados do utilizador (bem, na verdade, os eventos estão associados ao processo de submissão dos dados contidos no controlo). Para além destes evento, dispomos ainda do evento Authenticate. A autenticação dos utilizadores é precedida deste evento que nos fornece uma oportunidade de participar no processo de autenticação do utilizador. Se processarmos este evento, conseguimos influenciar o processo de validação do controlo. Normalmente tal não será necessário, uma vez que iremos recorrer à validação dos dados através da API de Membership (algo que este controlo faz automaticamente). Contudo, a existência deste evento permite-nos efectuar validações de utilizadores sem termos necessidade de utilizar um determinado provider. Já que falamos de providers, se quisermos podemos definir explicitamente o provider utilizado pelo controlo através da propriedade MembershipProvider.
Para terminarmos a análise a este controlo, queria ainda referir que podemos personalizar por completo o layout gerado. Tal como acontece com os controlos do tipo data bound (apresentados em artigos anteriores), temos apenas de atribuir determinados valores aos controlos utilizadas por forma a que o controlo continue a desempenhar as suas operações de forma automática. Assim , a caixa de texto que recebe o nome de utilizador deve possuir o ID username e a que recebe a palavra chave deve possuir o ID password. O botão que inicia o processo de validação dos dados introduzidos deve possuir o valor login atribuído à propriedade CommandName. A melhor forma de obter todos os IDs necessários consistem em visualizar o código HTML gerado pelo controlo. Uma alternativa mais árdua (mas que também produz mais frutos) consiste em analisar o código do controlo com recurso ao .Net Reflector).
Antes de terminarmos a secção relativa ao controlo Login, convém salientar que durante o processo de autenticação o controlo actualiza automaticamente o cookie de autenticação com os dados do utilizador que acabou de ser validado.
Controlo LoginName
Controlo mais simples do que este não há! Este controlo limita-se a apresentar o nome do utlizador. Após efectuarmos login, muitas vezes gostamos de apresentar uma saudação ao utilizador (por exemplo, 'Olá XXX'). Este controlo consegue formatar essa mensagem e apresentá-la ao utilizador. Se for necessário, podemos definir uma frase personalizada recorrendo ao place holder ({0}) para definir o local onde deve aparecer o nome do utilizador actual.
Controlo LoginStatus
O controlo LoginStatus fornece-nos um atalho para efectuarmos operações de login e logout de uma aplicação. A simplicidade deste controlo limita o número de propriedades apresentadas. Podemos configurar o texto ou imagem associada a cada uma das acções anteriores. Para além disso, podemos definir o comportamento da página associado ao evento de logout através da propriedade LogoutAction. Neste caso, podemos escolher uma de duas opções: navegação para a página de Login (LogoutAction.Redirect) e refrescamento da página actual (LogoutAction.Refresh).
Controlo LoginView
O controlo LoginView recorre a templates de forma a conseguir personalizar o conteúdo apresentado ao utilizador actual. Esta personalização, conseguida através da utilização de templates, permite a personalização de acordo com uma de três hipóteses:
- Utilizador anónimo: nesta situação, será sempre utilizado o template definido pela propriedade AnonymousTemplate. Esta opção será apresentada a utilizadores que não tenham sido previamente identificados;
- Utilizador autenticado: este template é utilizado sempre que seja detectado um utilizador identificado que não pertença a nenhum dos roles associados aos templates contidos no interior do elemento RolesGroup. Esta propriedade é definida através da propriedade LoggedInTemplate;
- Associação a um papel: o controlo possui uma propriedade especial (designada de RolesGroup) que permite definir vários templates associados a diferentes papeis (roles). Cada template deve ser definido através da propriedade ContentTemplate associada ao elemento do tipo RoleGroup. A propriedade Roles (deste elemento) permite definir a lista de roles à qual o template deve ser aplicado.
Convém ter em atenção que se um utilizador autenticado pertencer a um dos papeis associados a um template, então ser-lhe-á aplicado esse template (e não o template relativo ao utilizador autenticado). Por outro lado, um utilizador pode pertencer a vários papeis associados a diferentes templates. Nestas circunstâncias, será utilizado primeiro template válido da lista (por outras palavras, será aplicado o primeiro template associado a um role ao qual o utilizador pertence). Como ainda não falámos acerca do funcionamento dos papeis, não vamos utilizar esta opção na apresentação deste controlo.
Controlo ChangePassword
Uma das operações mais frequentes associadas às contas de utilizadores consiste na modificação da palavra chave. O controlo ChangePassword pode ser utilizado para efectuar esta alteração. Por defeito, este controlo apenas mostra três campos: palavra chave antiga, palavra chave nova e confirmação (desta última palavra chave). Se for necessário, podemos configurar o controlo por forma a que este também apresente um campo para introduzir o nome do utilizador. Esta última situação permite-nos modificar a palavra chave de qualquer utilizador existente na fonte de dados. Tal como acontece com alguns dos controlos anteriores, podemos recorrer à propriedade MembershipProvider para modificar o provider utilizado por este controlo.
Por defeito, o controlo funciona como um assistente, ou seja, o processo de modificação de palavra chave é constituído por dois passos: no primeiro, temos um layout que nos permite introduzir os dados associados à operação de alteração de palavra chave; o segundo passo consiste na geração de um template que indica que a operação foi efectuada com sucesso. Para além da configuração dos estilos apresentados pelo controlo (cuja investigação ficará a cargo do leitor), existe ainda a possibilidade de definirmos os nossos próprios templates. Tal como acontecia com o controlo Login, a utilização de controlos com nomes pré-definidos é necessária por forma a não termos de adicionar código que garanta o funcionamento do controlo (por outra palavras, se utilizarmos os nomes pré-definidos o controlo continua a executar as acções de forma correcta). No caso do controlo ChangePassword, podemos personalizar completamente o aspecto dos dois passos do controlo através dos elementos ChangePasswordTemplate e SuccessTemplate. Cada um destes elementos deve possuir controlos com IDs e CommandNames adequados (por exemplo, as caixas de texto utilizadas para apresentarem o nome de utilizador, palavra chave antiga e nova palavra chave devem possuir os valores username, currentpassword e newpassword; por outro lado, o botão de confirmação de nova palavra chave deve possuir o valor changepassword atribuído à propriedade CommandName).
Controlo PasswordRecovery
Se a modificação da palavra passe de um utilizador é algo comum, então o que dizer de eventuais operações de recuperação de palavras chave? Este controlo permite recuperar a palavra chave através do envio de um email. Este controlo apenas pode ser utilizado se a palavra chave for armazenada em formato Clear ou Encrypted. Se esta situação não for possível, então não será possível proceder à recuperação da palavra chave. A solução para este tipo de problemas consiste em definir uma nova palavra chave para o utilizador através do método Reset disponibilizado pela classe Membership.
O controlo consegue detectar se deve ou não requerer a introdução da resposta à pergunta de confirmação antes de proceder ao envio do email com a palavra chave. Tal como acontecia com os controlos anteriores, este controlo possui várias propriedades que permitem personalizar por completo o estilo do controlo em questão. Para além das propriedades de estilo, podemos ainda definir o aspecto total do controlo através da utilização de templates. Neste caso, temos três opções: UsernameTemplate (que permite personalizar o layout associado ao preenchimento do nome de utilizador), QuestionTemplate (utilizado para definir o aspecto do controlo na introdução da resposta à pergunta chave) e SuccessTemplate (utilizado para personalizar o aspecto do controlo se a operação de recuperação de palavra chave correr bem).
Controlo CreateUserWizard
Como seria de esperar, o pacote de controlos associados à segurança não estaria completo se não houvesse um controlo que permitisse a criação de novas contas de utilizador. Essa responsabilidade está a cargo do controlo CreateUserWizard. Este controlo, cuja classe base é a classe Wizard, permite-nos criar automaticamente novos utilizadores. A utilização simples deste controlo limita-se a gerar apenas um layout composto pelos vários campos necessários à criação de uma nova conta de utilizador. Por outras palavras, o controlo consegue verificar se a utilização da combinação pergunta recuperação/resposta é ou não obrigatória e se o endereço de e-mail deve continuar a ser único. Após analisar estas questões, o controlo apresenta automaticamente os campos necessários à criação de um novo utilizador. Para além disso, o controlo ainda fornece algumas possibilidades interessantes, como por exemplo, a autenticação automática do utilizador (configurado através da propriedade LoginCreatedUser) e/ou a crição de contas inactivas (propriedade DisableCreatedUser).
Como referi no parágrafo anterior, este controlo herda da classe Wizard. Esta classe permite a construção rápida de interfaces baseados em assistentes através da definição de vários templates que representam os vários passos necessários para concluir uma determinada acção. Por defeito, o controlo CreateUserWizard já possui dois templates por defeito que permitem introduzir os dados na nova conta e mostrar a mensagem de sucesso ao utilizador. Contudo, a utilização deste controlo irá obrigar de certeza à introdução de passos personalizados. Por exemplo, poderá ser necessário obter os dados relativos ao primeiro e último nome. Nestas situações, podemos introduzir passos adicionais mas não podemos remover os passos definidos por defeito (CreateUserStep e CompleteStep).
A criação de passos adicionais implica a adição de novos elementos da classe WizardStep no interior da propriedade WizardSteps. Um WizardStep possui duas áreas pré-definidas: ContentTemplate e CustomNavigationTemplate. Apesar dos nomes darem uma ideia correcta da sua funcionalidade, vou optar por acrescentar uma breve descrição acerca da utilização destes templates. O template ContentTemplate pode ser utilizado para definir o corpo principal do template, ou seja, para definirmos eventuais campos utilizados durante o passo actual do assistente. Por outro lado, o template definido pelo elemento CustomNavigationTemplate pode ser útil para personalizarmos os botões de navegação associados a um passo.
Como referi anteriomente, se adicionarmos passos personalizados ao assistente, temos sempre de manter a definição dos passos CreateUserStep e CompleteStep. Como seria de esperar (nesta altura) podemos também personalizar o aspecto destes passos pré-definidos. A utilização destas funcionalidades implica, como acontece com os controlos apresentados previamente, a utilização de IDs pré-definidos (só assim é possível manter o funcionamento correcto do controlo). A documentação que acompanha a versão CTP de Dezembro já contém os nomes e IDs num dos samples utilizados para documentar este controlo, pelo que não vou repetir essa informação aqui.
Para terminarmos a breve introdução a este controlo temos de falar ainda na barra de navegação que, por defeito, não é mostrada na página. Este elemento pode ser apresentado se atribuirmos o valor true à propriedade DisplaySideBar deste controlo. Esta barra é utilizada para orientar o utilizador ao longo do Wizard através da possibilidade de visualizar o passo actual que está a ser executado. Tal como aconteceu com os restantes controlos, se quisermos podemos personalizar este template através da utilização da propriedade SideBarTemplate. A única restrição associada a este template reside na necessidade de definirmos (no seu interior) um elemento do tipo DataList com o ID de SideBarList. Por sua vez, este elemento deverá conter um botão com o valor SideBarButton atribuído à propriedade CommandName contido no interior da propriedade ItemTemplate.
Envio de e-mails
Alguns dos controlos anteriores permitem o envio de emails de forma a completar determinadas acções. Normalmente, esses controlos possuem uma propriedade designada de MailDefinition que permite definir o conteúdo da mensagem que irá ser enviada. Se quisermos podemos enviar uma mensagem a partir de um determinado template. No caso dos controlos apresentados neste artigo, existem alguns valores especiais que são passados em determinadas mensagens. Por exemplo, o controlo PasswordRecovery possui os valores especiais <% Username %> e <% Password %>. Portanto, se quisermos construir um template para o envio de mensagens associadas à recuperação de palavra chave, podemos ter a certeza de que estes valores especiais serão substituídos pelo nome de utilizador e respectiva palavra chave.
Para enviarmos os mails, temos ainda de modificar o ficheiro de configuração. Na versão beta 1, esta configuração era feita através da utilização do elemento <smtpMail>. Apesar da documentação ainda apontar a utilização deste elemento, a verdade é que a partir da versão beta 2 o envio de emails passará a ser efectuado através da classe SmtpClient. Assim, as entradas necessárias à configuração deste elemento deverão ser efectuadas na secção system.net. O excerto seguinte apresenta um exemplo deste tipo de configurações:
<system.net>
<mailSettings>
<smtp>
<network host="mail.netmadeira.com"/>
</smtp>
</mailSettings>
</system.net>
Infelizmente, ao construir o sample que acompanha este artigo, não consegui resolver os erros associados ao envio de e-mails. Uma busca ao site de feedback da Microsoft levou-me a concluir que a versão actual possui alguns bugs associados à classe SmtpClient . Agora presumo que vamos ter de esperar pela beta 2 para saber se os problemas ocorridos estão relacionados com a versão actual ou apenas com a incapacidade do autor de efectuar as configurações correctas no ficheiro de configuração (claro que o autor está optimista e por isso espera - ou melhor, reza - para que sejam mesmo bugs :) ).
Namespace System.Web.Administration
Para além de todos estes controlos e da nova API apresentados até ao momento, a nova plataforma reserva-nos ainda uma supresa agradável: estou a falar do namespace System.Web.Administration. Este namespace contém várias classes que nos auxiliam na construção de sites de administração. Na verdade, a nova ferramenta web que acompanha a versão 2.0 da plataforma é construída com a ajuda das várias classes contidas neste namespace. Apesar de ainda não ter tido o tempo suficiente para proceder à investigação adequada das várias classes existentes aqui, existem algumas cuja utilização é óbvia. Por exemplo, a classe WebAdminBasePage deverá ser utilizada como classe base de eventuais páginas administrativas. Esta classe possui vários métodos interessantes, como por exemplo, CreateAccessFile. Este método consegue criar o ficheiro de Access necessário aos providers AccessXXX. A investigação dos restantes controlos contidos neste namespace poderá vir a ser o alvo dum artigo futuro (algo que provavelmente irei fazer assim que recuperar o meu portátil :( )
Conclusões finais
Ao longo deste artigo continuámos o nosso estudo iniciado há cerca de uma semana. Começámos por identificar o esquema utilizado pelos providers actuais para armazenamento dos dados. Falámos também sobre os novos controlos de segurança e respectiva utilização. A personalização dos controlos pode ser efectuada de forma rápida pois (praticamente) todos eles suportam a utilização de templates. É claro que este tipo de operações obriga a que o programador utilize determinados valores para certas propriedades dos controlos utilizados nesta personalização. Num próximo artigo iremos terminar a análise aos novos mecanismos de segurança efectuando a construção de um provider personalizado que irá efectuar todas as operações descritas até ao momento.
O código que acompanha este artigo irá ser disponibilizado na secção de downloads do site (está a aguardar autorização). Por favor enviem-me as vossas opiniões/sugestões/críticas/correcções para progC@netmadeira.com.
Fiquem bem e boa programação! Até à próxima.
Leiam o meu blog em: http://weblogs.pontonetpt.com/luisabreu