Desenvolvimento - ASP. NET
ASP.NET 2.0 – Customizando a segurança de suas aplicações
Veja nesse artigo como customizar a segurança de aplicações ASP.NET 2.0, adaptando-a em suas bases de dados de acordo com sua necessidade.
por Andrey SanchesPorque customizar a segurança?
O lançamento da tecnologia ASP.NET 2.0 revolucionou ainda mais o mercado de desenvolvimento de aplicações WEB comparando-se com o ASP.NET 1.x, hoje já substituído em grande parte dos projetos. Um dos principais recursos adicionados a produtividade dessa tecnologia, foi a forma com que foram desenvolvidas os Frameworks de Segurança, Personalização e outros que não comentarei nesse artigo. Esses Frameworks, desenvolvidos para facilitar a criação de aplicações, agilizam muito o processo de configurações de tarefas do nosso dia-a-dia como: Cadastro de Usuários, Gerenciamento de Perfis de acesso, Gerenciamento de senhas, etc.
Para dar suporte à esses Frameworks, a Microsoft criou diversos controles (Login, CreateUserWizard, ChangePassword e outros), que por sua vez utilizam de um Provider para execução das tarefas relacionadas acima.
Nesse artigo não vou entrar muito em detalhes de como funcionam esses providers, mas se você tiver interesse em conhecer mais sobre a arquitetura, recomendo a leitura do artigo Entendendo e Implementando Segurança no ASP.NET 2.0 do Israel Aéce.
A grande dificuldade que tenho acompanhado nesses quase 2 anos de ASP.NET 2.0 (que inclusive foi um dos motivos que me fizeram escrever esse artigo), foi como se desprender dos padrões dos providers atuais e adaptá-los totalmente à necessidade de uma aplicação WEB, utilizando-se assim de toda a infra-estrutura de Segurança do ASP.NET 2.0 e customizando o que é necessário para o cenário, persistindo os dados de usuários em uma base de dados própria.
Vamos então para um cenário real, onde teremos um cadastro de usuários em uma base de dados criado por nós, desenvolvedores, podendo incluir as informações da forma que bem entendermos.
Modelo da Base de Dados
Para demonstrar esse recurso na prática, criei um tabela de usuários conforme a minha necessidade em uma base de dados do SQL-SERVER 2005, a qual é exibida na Figura 1 com todos os campos necessários para cadastrar um usuário do sistema. Veja que logo no princípio, violamos a regra padrão do SqlMemberShipProvider do ASP.NET 2.0, onde essas informações estariam em duas tabelas, sendo elas aspnet_Users e aspnet_Membership contidas no banco de dados que por padrão tem o nome de aspnetdb.
Figura 1 – Tabela de Usuários personalizada
Com a tabela de usuários pronta, o que você precisa fazer agora são as classes necessárias para persistirem dados nessa tabela, utilizando a infra-estrutura de segurança do ASP.NET 2.0. Para isso, criaremos então um provider que fará uso dos recursos existentes, porém com algumas modificações.
Porque criar o seu próprio provider MemberShip ?
O motivo principal da criação de um provider é a necessidade de customizar configurações que o provider padrão não disponibilizou, já que seria praticamente impossível um modelo padrão atender as necessidades diversas que encontramos no dia-a-dia. Sendo assim, a criação do provider se torna algo necessário e pode ser feito seguindo alguns simples passos para a utilização posterior.
À primeira vista, a criação de um provider demonstra ser uma tarefa complexa, porém você verá que o processo não é tão difícil como parece, porém, será necessário principalmente o entendimento de conceitos de Programação Orientada à Objetos para que fique claro o comportamento de um provider.
O atual modelo de objetos do Membership pode ser encontrado no artigo citado anteriormente, contendo as classes que cuidam de operações de usuários como: Cadastrar, Excluir, Localizar, Validar e outras operações que sempre precisamos no dia-a-dia de uma aplicação Veja que a classe SqlMembershipProvider é um provider especializado para SQL-SERVER, a qual herda de MembershipProvider, a classe base para um provider de Membership.
Existe ainda a classe MembershipUser (não presente no diagrama), uma classe do Framework .Net que por sua vez, disponibiliza informações de um usuário em específico como: UserName, Email, ProviderKey, etc.
A Listagem 1 demonstra a classe UserManager, criada para manipular a tabela de usuários, fazendo o papel da classe SqlMemberShipProvider citada. Veja que a partir do momento que herdamos da classe MemberShipProvider, temos total poder perante o código o que nos possibilita persistir os dados da forma que for necessária. Da mesma forma apresento na Listagem 2 a classe UserInfo, substituindo a classe MembershipUser para atender diretamente as necessidades da base de dados proposta.
using System; using System.Web; using System.Web.Security; using System.Data.SqlClient; using System.Configuration; namespace MyCustomMembershipProvider { public class UserManager: MembershipProvider {
} }
|
Listagem 1 – Especializando a classe Membership do ASP.NET 2.0
using System; using System.Web.Security;
namespace MyCustomMembershipProvider { public class UserInfo: MembershipUser
{ private int _codigo; private string _nome; private string _setor; private string _empresa; private string _endereco; private string _login;
public string Nome {get {return _nome; }set { _nome = value;}} public string Setor{get {return _setor;}set { _setor = value;}} public string Empresa {get {return _empresa;}set {_empresa = value;}} public string Endereco{get { return _endereco;}set{_endereco = value;}} } } |
Listagem 2 – Especializando a classe MembershipUser padrão do ASP.NET 2.0
Tendo a classe herdada, basta agora iniciar a subscrição dos métodos que deseja personalizar. Os passos que irei listar são somente os principais para que o seu provider funcione corretamente, e é claro que você poderia subscrever outros métodos, ou até mesmo todos os métodos da classe herdada. Siga os passos e ao final você terá o provider proposto no início desse artigo.
ps: Mesmo modificando a assinatura de alguns
(poucos) métodos, o objetivo é preservar ao máximo
a estrutura da baseclasse, customizando somente a implementação dos métodos necessários.
Passo 1 - CreateUser
O método CreateUser padrão da classe MembershiProvider não atende a necessidade do modelo de dados proposto. Sendo assim, precisei criar um novo método com uma assinatura diferente da classe padrão:
public
MembershipUser CreateUser(string nome,
string setor, string
empresa,
string endereco, string
email, string login, string
senha,
out MembershipCreateStatus retorno)
string SQLInsert = null; SQLInsert = "INSERT INTO Usuario (Nome, Setor, Empresa, Endereco, Email, Login, Senha) values "; SQLInsert += "(@Nome, @Setor, @Empresa, @Endereco, @Email, @Login, @Senha)"; SqlCommand command = new SqlCommand(SQLInsert, conn);
command.Parameters.Add(new SqlParameter("@Nome",nome)); command.Parameters.Add(new SqlParameter("@Setor",setor)); command.Parameters.Add(new SqlParameter("@Empresa",empresa)); command.Parameters.Add(new SqlParameter("@Endereco",endereco)); command.Parameters.Add(new SqlParameter("@Email",email)); command.Parameters.Add(new SqlParameter("@Login",login)); command.Parameters.Add(new SqlParameter("@Senha",senha));
try { command.ExecuteNonQuery();
//retorno o parâmetro "out" executado com sucesso retorno = MembershipCreateStatus.Success;
//retorna o usuário com todos os seus dados return System.Web.Security.Membership.GetUser(login); } catch (Exception ex) { throw; } } |
Passo 2 - DeleteUser
O método DeleteUser deve ser reescrito para que a exclusão do Usuário aconteça na tabela correta de sua base de dados. Veja que o método é subscrito utilizando a chave override, a qual substitui o funcionamento do método em sua baseclass e assume o presente nessa classe.
public override bool DeleteUser(string username, bool deleteAllRelatedData) {
SqlCommand command = new SqlCommand("DELETE FROM Usuario WHERE login = @login", conn);
command.Parameters.Add(new SqlParameter("@login",username)); try { command.ExecuteNonQuery(); if (deleteAllRelatedData) { //instruções para exclusão de todas as tabelas dependentes } return true; } catch (Exception) { return false; } } |
Passo 3 - GetUser
O método GetUser da mesma forma é subscrito para localizar o usuário na base de dados, ao final retorna o objeto padrão MembershipUser com as informações do usuário localizado. Observe que é retornado um objeto do tipo UserInfo, isso é possível pois ele herda de MembershipUser.
public override MembershipUser GetUser(string username, bool userIsOnline) { SqlCommand command = new SqlCommand("SELECT * FROM Usuario WHERE Login = @login", conn); command.Parameters.Add(new SqlParameter("@login", username)); SqlDataReader reader = command.ExecuteReader(System.Data.CommandBehavior.CloseConnection);
UserInfo usuario = new serInfo(reader["login"].ToString(), (int)reader["codigo"], reader["nome"].ToString());
return usuario;
} |
Passo 4 - UpdateUser
O método UpdateUser recebe por parâmetro um objeto do tipo MembershipUser que será usado nos parâmetros de atualização. Pelo motivo de utilizarmos um objeto que herda de MembershipUser, é necessário efetuar a conversão na recuperação dos valores para seja possível ter acesso às propriedades do objeto UserInfo.
public override void UpdateUser(MembershipUser user) {
SqlCommand command = new SqlCommand(); command.Connection = conn; string SQLUpdate = "UPDATE Usuario set Nome = @Nome, "; SQLUpdate += " Setor = @Setor, "; SQLUpdate += " Empresa = @Empresa, "; SQLUpdate += " Endereco = @Endereco, "; SQLUpdate += " Email = @Email, "; SQLUpdate += " WHERE Codigo = @Codigo"; command.CommandText = SQLUpdate;
command.Parameters.Add(new SqlParameter("@Nome",((UserInfo)user).Nome)); command.Parameters.Add(new SqlParameter("@Setor", ((UserInfo)user).Setor)); command.Parameters.Add(new SqlParameter("@Empresa", ((UserInfo)user).Empresa)); command.Parameters.Add(new SqlParameter("@Endereco", ((UserInfo)user).Endereco)); command.Parameters.Add(new SqlParameter("@Email", ((UserInfo)user).Email)); command.Parameters.Add(new SqlParameter("@Codigo", user.ProviderUserKey));
command.ExecuteNonQuery();
} |
Passo 5 – ValidateUser
Já o método ValidateUser, efetua a autenticação na base de dados retornando um valor boleano indicando se encontrou o registro do usuário e senha passados por parâmetro.
public override bool ValidateUser(string username, string password) { SqlCommand command = new SqlCommand("SELECT login FROM Usuario WHERE Login = @login and Senha = @ senha",conn);
command.Parameters.Add(new SqlParameter("@login", username)); command.Parameters.Add(new SqlParameter("@senha", password));
SqlDataReader reader = command.ExecuteReader(System.Data.CommandBehavior.CloseConnection);
return reader.HasRows; } |
Passo 6 – Propriedades diferenciais da classe UserInfo
Como você pôde perceber, a customização foi além da classe UserManager, ainda é necessário customizar a classe UserInfo para atender a estrutura de usuários da base de dados. Para isso, utilizei a propriedade ProviderUserKey do objeto MembershipUser para retornar o valor do atributo _codigo da classe. Da mesma forma utilizei da propriedade UserName para retornar o valor do atributo _login. Esses dois atributos foram criados pois minha necessidade era que eu pudesse configurar os valores deles livremente, porém a classe MembershipUser não me disponibilizou métodos para tal, porém veja que conservei os nomes das propriedades, subscrevendo-as e mudando somente a implementação de cada propriedade.
public UserInfo(string userName, int providerUserKey, string nome) { _login = userName; _codigo = providerUserKey; _nome = nome;
} public override object ProviderUserKey { get { return _codigo; }
} public override string UserName { get { return _login; } } |
Como você pôde perceber, não subscrevi TODOS os métodos das classes MembershipProvider e MembershipUser pois tenho certeza que você já conseguiu entender o benefício desse processo. Os métodos que não foram exibidos, tem sua implementação somente chamando o mesmo objeto de sua baseclass.
O método Initialize
Todo provider tem como regra de implementação o método Initialize. Esse método é chamado automaticamente quando o provider é invocado. Normalmente utilizamos o método construtor das classes para executar operações iniciais quando o objeto é instanciado. O método Initialize tem um comportamento similar, é executado quando o provider é instanciado, para isso, ele disponibiliza dois parâmetros contendo o nome (name) do provider e um objeto do tipo NameValueCollection que é uma coleção com chave/valor de informações que podem ser configuradas no web.config e utilizadas como variáveis ou qualquer outra forma de utilização que seu provider necessitar. Veja na listagem abaixo a configuração do provider identificando o nome na chave name, o namespace.classe na chave, a identificação e logo em seguida a utilização dos valores no método Initialize
type = namespace.classe, assembly
connectionStringName = string de conexão que o provider utilizará
<membership defaultProvider="CustomMembershipProvider"> <providers> <clear/> <add name="CustomMembershipProvider" type="MyCustomMembershipProvider.UserManager, CustomMembershipProvider" connectionStringName="MinhaConexaoSQL" Valor1="valor1" Valor2="valor2"/>
</providers> </membership> |
public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config) { conn.ConnectionString = ConfigurationManager.ConnectionStrings[config["connectionStringName"]].ToString(); conn.Open();
string valor1 = config["Valor1"]; string valor2 = config["Valor2"]; }
|
Veja que as chaves Valor1 e Valor2 foram incluídas no web.config, na seção da configuração do provider e utilizadas dinamicamente no método Initialize para efetuar qualquer operação baseado nos valores recebidos. Da mesma forma foi utilizado o atributo connectionStringName apontando para uma string de conexão válida no web.config a qual faz a conexão com o banco de dados em questão.
A implementação nas páginas
Agora que as classes no provider já estão finalizadas, basta utilizá-las normalmente da mesma forma que você já está acostumado.
Para ilustrar com um exemplo mais prático, criei um cadastro de usuários onde é implementando o método CreateUser para demonstração. Não implementarei os outros métodos pois daí pra frente você já sabe como fazer. Veja na Figura 2 o leiaute da página de cadastro de usuários e logo em seguida na Listagem 3 o acesso ao provider para manipulação do usuário.
Figura 2 – Cadastro de usuários customizado
protected void btConfirmar_Click(object sender, EventArgs e) { UserManager user = (UserManager)Membership.Provider;
MembershipCreateStatus status; user.CreateUser(txtNome.Text, txtSetor.Text, txtEmpresa.Text, txtEndereco.Text, txtEmail.Text, txtLogin.Text, txtSenha.Text, out status);
if (status == MembershipCreateStatus.Success) lblMensagem.Text = "Usuário criado com sucesso !!!"; else lblMensagem.Text = "Aconteceu um erro !!!"; }
|
Listagem 3 – Implementação do método CreateUser a partir do Provider criado
A primeira linha da Listagem 3 utiliza da propriedade Provider para recuperar o objeto padrão Membership (especificado no web.config, defaultProvider) que está instanciado no momento e converte para o tipo criado, no caso o UserManager. Recuperando a instância do provider, executamos o método CreateUser do objeto em questão, passando por parâmetro os valores necessários. Após a execução, o objeto status retornará o resultado da criação do usuário no banco de dados.
Execute a página, preencha os campos, clique no botão “Confirmar” e veja o resultado.
Conclusão
Como você pôde perceber, a customização do provider Membership é muito simples e pode se adequar 100% a sua necessidade. Agora você já conseguirá customizar seu provider de segurança e se desprender dos providers padrões no ASP.NET 2.0. Essa customização pode ser feito também para outros bancos de dados ou até mesmo para outros modelos diferentes deste que foi mostrado. Boa customização e divirta-se.