Controle de Usuários para sistemas ASP.NET - Parte 2 - Classe de Perfis de Usuário
Olá pessoal! Como dito no post anterior, agora vamos modelar e desenvolver a classe que manipulará os Perfis de Usuário do nosso controle de acesso.
Ela fará as operações de inserção, atualização e exclusão de perfis e controlará também quais os módulos do sistema serão acessados por aquele perfil.
Para isso, criei uma classe com o nome de TPerfilUsuario (o "T" como prefixo do nome da classe foi herdado do meu lado Delphero hehe ;-) ) e contém os seguintes campos e propriedades:
1: public class TPerfilUsuario : Conexao
2: {
3: #region Parte VO
4: private int _PERFIL_ID = 0;
5: private string _DESCRICAO = "";
6: private string _SearchSQL = "";
7: private List<TPerfilModulo> _Modulos = TPerfilModulo.ListarModulosPerfil(0);
8:
9: public int PerfilId { get { return _PERFIL_ID; } set { _PERFIL_ID = value; } }
10: public string Descricao { get { return _DESCRICAO; } set { _DESCRICAO = value; } }
11: public List<TPerfilModulo> Modulos { get { return _Modulos; } }
12: #endregion
13: ...
14: }
Como vocês podem ver, ela é herdada da classe Conexao, que corresponde a classe principal do framework apresentado nos posts iniciais do NeoMatrix Tech e que manipula os objetos dos providers do banco de dados. Temos como campos ligados diretamente ao BD os campos _PERFIL_ID e _DESCRICAO, devidamente inicializados. Temos também um campo privado chamado _SearchSQL que utilizaremos para a construção do método de listagem de perfis (para apresentação em gridview, por exemplo) e um campo do tipo List<TPerfilModulo>, que listará os módulos do sistema e a respectiva permissão de acesso.
Para explicar melhor, vamos abrir um parêntese na explicação da classe TPerfilUsuario.
No nosso banco de dados temos uma tabela chamada PERFIL_MODULOS, que relaciona cada perfil de usuário com os módulos que ele pode acessar. Esta classe é um espelho desta tabela e possui a seguinte estrutura:
1: public class TPerfilModulo : Conexao
2: {
3: #region Parte VO
4: private int _MODULO_ID = 0;
5: private string _DESCRICAO = "";
6: private int _PODE_ACESSAR = 0;
7: private string _PAGINA_WEB = "";
8:
9: public int ModuloId { get { return _MODULO_ID; } }
10: public string Descricao { get { return _DESCRICAO; } }
11: public int PodeAcessar { get { return _PODE_ACESSAR; } set { _PODE_ACESSAR = value; } }
12: public string PaginaWeb { get { return _PAGINA_WEB; } }
13: #endregion
14:
15: #region Parte DAO
16: private List<TPerfilModulo> ListarTodos(int pPerfilID)
17: {
18: List<TPerfilModulo> l = new List<TPerfilModulo>();
19: string isql = "select sm.modulo_id,sm.descricao,case coalesce(pm.perfil_id,0) when 0 then 0 else 1 end as PODE_ACESSAR,sm.pagina_web from sys_modulos sm " +
20: "left join perfil_modulos pm on sm.modulo_id = pm.modulo_id and pm.perfil_id = @PID ";
21: ClearSQLParams();
22: AddSQLParam("@PID", pPerfilID, ParameterDirection.Input);
23: DataTable dt = this.getTable(isql, true);
24: foreach (DataRow r in dt.Rows)
25: {
26: l.Add(new TPerfilModulo());
27: l[l.Count - 1]._MODULO_ID = Consts.Funcoes.NullOrInt(r["MODULO_ID"]);
28: l[l.Count - 1]._DESCRICAO = Consts.Funcoes.NullOrString(r["DESCRICAO"]);
29: l[l.Count - 1]._PODE_ACESSAR = Consts.Funcoes.NullOrInt(r["PODE_ACESSAR"]);
30: l[l.Count - 1]._PAGINA_WEB = Consts.Funcoes.NullOrString(r["PAGINA_WEB"]);
31: }
32: return l;
33: }
34: #endregion
35:
36: #region Parte BO
37: public static List<TPerfilModulo> ListarModulosPerfil(int pPerfilID)
38: {
39: List<TPerfilModulo> pm = new List<TPerfilModulo>();
40: TPerfilModulo p = new TPerfilModulo();
41: try
42: {
43: pm = p.ListarTodos(pPerfilID);
44: }
45: finally
46: {
47: p.Dispose();
48: }
49: return pm;
50: }
51:
52:
53: #endregion
54: }
Temos como propriedades o ID do módulo, a descrição, o flag de acesso e o nome do formulário .aspx.
Também há um método privado que retorna uma lista dos módulos, porém filtrada para um determinado perfil de acesso. Para construir essa lista, é montado um comando SQL que popula um DataTable, e para cada linha deste DataTable é criada uma instância da classe TPerfilModulo e inserida no List de retorno. Este método é exposto para as outras classes através do método estático ListarModulosPerfil. Ele foi construído como estático para não precisarmos criar uma instância de TPerfilModulo somente para retornar a pesquisa :-)
A query feita merece um destaque:
1: string isql = "select sm.modulo_id,sm.descricao,case coalesce(pm.perfil_id,0) when 0 then 0 else 1 end as PODE_ACESSAR,sm.pagina_web from sys_modulos sm " +
2: "left join perfil_modulos pm on sm.modulo_id = pm.modulo_id and pm.perfil_id = @PID ";
Faço a instrução SELECT em cima da tabela SYS_MODULOS, que se usada isoladamente retorna todos os módulos inseridos nesta tabela. Dela, listo os campos MODULO_ID e DESCRICAO. Para obter as permissões de acesso para cada perfil, faço um left join com a tabela PERFIL_MODULOS, pelo campo MODULO_ID de ambas as tabelas. Com esta cláusula, ele lista uma ocorrência de um registro na tabela SYS_MODULOS para cada ocorrência da tabela USR_PERFIL, mesmo que não haja correspondência para a combinação perfil x módulo na tabela PERFIL_MODULOS, que resultará em um estado "null" para o campo PERFIL_ID da tabela PERFIL_MODULOS. Para poder listar SOMENTE um determinado perfil nesta query, acrescento a condição "... and PM.PERFIL_ID = @PID" na cláusula de join, onde o parâmetro @PID é o código do perfil. Se fizessemos esse filtro na cláusula WHERE, não funcionaria, a query apenas mostraria os registros existentes na tabela PERFIL_MODULOS.
Como foi feito um left join e todas as ocorrências da tabela SYS_MODULOS são listadas mesmo que não haja uma correspondência de perfil na tabela PERFIL_MODULOS, o campo PERFIL_ID para a tabela PERFIL_MODULOS ficará nulo. Para construir um flag de acesso "1" para "Tem Acesso" e "0" para "Sem Acesso", fiz um coalesce no campo PERFIL_ID. Se for nulo, será zero, caso contrário, receberá 1 (a correspondência perfil x módulo existe na tabela PERFIL_MODULOS). Resumindo: Se existe um PERFIL_ID no resultado da query, o perfil tem acesso ao módulo correspondente; caso esteja nulo, não terá acesso.
Fechado os parênteses, podemos continuar com a classe TPerfilUsuario :-)
Vamos analisar o restante do código da classe TPerfilUsuario:
1: ...
2: #region Parte DAO
3:
4:
5: public void SetByID()
6: {
7: try
8: {
9: this._SelectSQL = "select * from USR_PERFIL where PERFIL_ID = @PERFIL_ID ";
10: ClearSQLParams();
11: AddSQLParam("@PERFIL_ID", this._PERFIL_ID, ParameterDirection.Input);
12:
13: this.Select(true);
14: if (this.ListaCamposTabela.Count > 0)
15: {
16: this._PERFIL_ID = Consts.Funcoes.NullOrInt(this["PERFIL_ID"]);
17: this._DESCRICAO = Consts.Funcoes.NullOrString(this["DESCRICAO"]);
18: _Modulos = TPerfilModulo.ListarModulosPerfil(_PERFIL_ID);
19: }
20: else
21: {
22: this.fMsgInfo = "Registro não encontrado";
23: }
24: }
25: catch (Exception ex)
26: {
27: this.fMsgInfo = "Erro ao obter dados -> " + ex.Message;
28: }
29: }
30:
31: public bool Inserir()
32: {
33: bool st = false;
34: ClearSQLParams();
35: AddSQLParam("@PERFIL_ID", Consts.Funcoes.ZeroOrDBNull(this._PERFIL_ID), ParameterDirection.Output);
36: AddSQLParam("@DESCRICAO", Consts.Funcoes.ValueOrDBNull(this._DESCRICAO.ToUpper().Trim()), ParameterDirection.Input);
37: List<TCampoCadastro> result = executeStoredProcedure("SP_INSERE_PERFIL", true);
38:
39: if (result.Count > 0)
40: {
41: fMsgInfo = "Inserido com sucesso!";
42: _PERFIL_ID = Consts.Funcoes.NullOrInt(result[0].Valor);
43:
44: //Salva os módulos com permissão
45: ClearSQLParams();
46: foreach (TPerfilModulo p in _Modulos)
47: {
48: string isql = "execute procedure SP_SALVA_PERFIL_MODULO(@PERFIL_ID,@MODULO_ID,@TEM_ACESSO)";
49: ArrayList paramssql = new ArrayList();
50: AddSQLParam("@PERFIL_ID", _PERFIL_ID, ParameterDirection.Input, ref paramssql);
51: AddSQLParam("@MODULO_ID", p.ModuloId, ParameterDirection.Input, ref paramssql);
52: AddSQLParam("@TEM_ACESSO", p.PodeAcessar, ParameterDirection.Input, ref paramssql);
53: AdvancedBatchSQLItens.Add(new TBatchSQLItens(isql, paramssql));
54: }
55: execAdvancedBatchSQL();
56: st = true;
57: }
58: return st;
59: }
60:
61: public bool Atualizar()
62: {
63: bool st = false;
64: _UpdateSQL = "UPDATE USR_PERFIL set PERFIL_ID = @PERFIL_ID, DESCRICAO = @DESCRICAO where PERFIL_ID = @PERFIL_ID ";
65: ClearSQLParams();
66: AddSQLParam("@PERFIL_ID", Consts.Funcoes.ZeroOrDBNull(this._PERFIL_ID), ParameterDirection.Input);
67: AddSQLParam("@DESCRICAO", Consts.Funcoes.ValueOrDBNull(this._DESCRICAO.ToUpper().Trim()), ParameterDirection.Input);
68:
69: if (st = Salvar(OperacaoBD.opUpdate, true))
70: {
71: //Salva os módulos com permissão
72: ClearSQLParams();
73: foreach (TPerfilModulo p in _Modulos)
74: {
75: string isql = "execute procedure SP_SALVA_PERFIL_MODULO(@PERFIL_ID,@MODULO_ID,@TEM_ACESSO)";
76: ArrayList paramssql = new ArrayList();
77: AddSQLParam("@PERFIL_ID", _PERFIL_ID, ParameterDirection.Input, ref paramssql);
78: AddSQLParam("@MODULO_ID", p.ModuloId, ParameterDirection.Input, ref paramssql);
79: AddSQLParam("@TEM_ACESSO", p.PodeAcessar, ParameterDirection.Input, ref paramssql);
80: AdvancedBatchSQLItens.Add(new TBatchSQLItens(isql, paramssql));
81: }
82: execAdvancedBatchSQL();
83: fMsgInfo = "Atualizado com sucesso!";
84: }
85: return st;
86: }
87:
88: public bool _Excluir()
89: {
90: _DeleteSQL = "delete from USR_PERFIL where PERFIL_ID = @PERFIL_ID ";
91: ClearSQLParams();
92: AddSQLParam("@PERFIL_ID", Consts.Funcoes.ZeroOrDBNull(this._PERFIL_ID), ParameterDirection.Input);
93:
94: bool st = false;
95: if (st = Salvar(OperacaoBD.opDelete, true))
96: {
97: fMsgInfo = "Excluído com sucesso!";
98: }
99: return st;
100: }
101:
102: private List<TPerfilUsuario> ListaGenerica()
103: {
104: List<TPerfilUsuario> l = new List<TPerfilUsuario>();
105: DataTable dt = this.getTable(_SearchSQL, (SQLParams.Count > 0));
106: foreach (DataRow r in dt.Rows)
107: {
108: l.Add(new TPerfilUsuario());
109: l[l.Count - 1].PerfilId = Consts.Funcoes.NullOrInt(r["PERFIL_ID"]);
110: l[l.Count - 1].Descricao = Consts.Funcoes.NullOrString(r["DESCRICAO"]);
111: }
112: return l;
113: }
114:
115: public List<TPerfilUsuario> ListarTodos()
116: {
117: _SearchSQL = "select * from USR_PERFIL";
118: return ListaGenerica();
119: }
120:
121: #endregion
122:
123:
124: #region Parte BO
125:
126: public List<TPerfilUsuario> Listar(string pDescricao)
127: {
128: _SearchSQL = "select * from USR_PERFIL where (1=1) ";
129: if (!pDescricao.Trim().Equals(""))
130: {
131: _SearchSQL += " and DESCRICAO like @DESC ";
132: AddSQLParam("@DESC", pDescricao.ToUpper().Trim() + "%", ParameterDirection.Input);
133: }
134: return ListaGenerica();
135: }
136:
137: public static List<TPerfilUsuario> ListarPerfis(string pDescricao)
138: {
139: List<TPerfilUsuario> l = new List<TPerfilUsuario>();
140: TPerfilUsuario u = new TPerfilUsuario();
141: try
142: {
143: l = u.Listar(pDescricao);
144: }
145: finally
146: {
147: u.Dispose();
148: }
149: return l;
150: }
151:
152: public static bool Excluir(int pID, out string msg)
153: {
154: TPerfilUsuario p = new TPerfilUsuario();
155: bool st = false;
156: try
157: {
158: p._PERFIL_ID = pID;
159: st = p._Excluir();
160: msg = p.MsgInfo;
161: }
162: finally
163: {
164: p.Dispose();
165: }
166: return st;
167: }
168:
169: public bool TemAcesso(string pNomeForm)
170: {
171: bool r = false;
172: foreach (TPerfilModulo m in _Modulos)
173: {
174: if (m.PaginaWeb.ToUpper().Equals(pNomeForm.ToUpper()) && m.PodeAcessar.Equals(1))
175: {
176: r = true;
177: break;
178: }
179: }
180:
181: return r;
182: }
183:
184:
185: #endregion
186: ...
Temos o método SetByID, que alimenta a instância corrente com os demais dados do perfil indicado no campo _PERFIL_ID. Portanto, para carregarmos um determinado perfil, primeiro alimentamos seu ID na propriedade PerfilId e depois chamamos o método SetByID. No SetByID também alimentamos o list Modulos, chamando o método estático ListarModulosPerfil da classe TPerfilModulo, recebendo como parâmetro o perfil que acabamos de consultar.
O método Inserir faz a inserção do perfil de usuário na tabela USR_PERFIL através da stored procedure SP_INSERE_PERFIL, utilizando o método executeStoredProcedure da Classe Conexao. Se for bem sucedido, é retornado um List com a propriedade Count > 0, com objetos do tipo TCampoCadastro que representarão cada parâmetro da stored procedure acrescentado através do método AddSQLParam.
Utilizaremos este List para pegar o valor do parâmetro de saída PERFIL_ID retornado pela SP, que foi o primeiro a ser acrescentado através do método AddSQLParam e portanto tem o índice "0" no list.
Se a SP foi executada com sucesso, guardamos o perfil retornado no campo _PERFIL_ID e em seguida gravamos as permissões de acesso. Na interface do usuário, quando é gravado um perfil, ele popula a propriedade Modulos de TPerfil com cada módulo que desejamos que aquele perfil tenha acesso, alimentando com o valor "1" a propriedade PodeAcessar de cada módulo (veremos isso mais para frente :-) ). Voltando às vacas frias... depois de confirmada a gravação do perfil (para não violar nenhuma foreign key depois :P ) varremos a propriedade Modulos, em busca das propriedades ModuloID e PodeAcessar de cada objeto TPerfilModulo. Para tal, é feito um foreach em cima desta lista e montamos uma instrução SQL para cada item. Esta instrução SQL executará a instrução SQL SP_SALVA_PERFIL_MODULO (note, não usarei o método executeStoredProcedure aqui), sendo que cada comando SQL é parametrizado. Para cada comando, um ArrayList com os parâmetros da consulta é criado, e tanto o comando SQL como o ArrayList de parâmetros são adicionados no campo AdvancedBatchSQLItens da classe Conexao, através de uma instância da classe TBatchSQLItens (ver o artigo da classe de conexão para maiores esclarecimentos). Por fim, estes comandos SQL são executados em uma única transação, ou seja, se um comando der pau, nada é gravado no banco.
O método Atualizar funciona de maneira análoga ao Inserir, as diferenças são o comando e a forma de execução do SQL: o comando é um UPDATE parametrizado e é executado através do método Salvar, que recebe como parâmetro a identificação de uma operação de Update e true, indicando que possui parâmetros.
Se o update for executado com sucesso, as permissões de acesso são salvas da mesma forma do método Inserir.
O método Excluir não tem mistério... ele executa uma instrução DELETE comum através do método Salvar de Conexao.
Vamos dar um destaque ao método ListaGenerica:
1: private List<TPerfilUsuario> ListaGenerica()
2: {
3: List<TPerfilUsuario> l = new List<TPerfilUsuario>();
4: DataTable dt = this.getTable(_SearchSQL, (SQLParams.Count > 0));
5: foreach (DataRow r in dt.Rows)
6: {
7: l.Add(new TPerfilUsuario());
8: l[l.Count - 1].PerfilId = Consts.Funcoes.NullOrInt(r["PERFIL_ID"]);
9: l[l.Count - 1].Descricao = Consts.Funcoes.NullOrString(r["DESCRICAO"]);
10: }
11: return l;
12: }
Ele vai ser a base de todos os métodos de consulta. Este método popula um datatable através da instrução SQL contida no campo _SearchSQL. Aqui devo abrir outro parêntese: Note que o parâmetro "HaveParams" do método getTable é calculado com base na propriedade Count da propriedade SQLParams da classe Conexao. Na primeira versão da classe Conexao publicada aqui, este campo era declarado como private, portanto, sem acesso nas outras classes (ele era acessível somente pelos métodos AddSQLParam). Para fazer tal cálculo, alterei a restrição de acesso para protected.
Fechado mais esse parêntese hehe...
Para cada linha do datatable, é criada uma instância de TPerfilUsuario no list de retorno, e as propriedades desta instância são populadas pelos campos correspondentes na linha do DataTable.
Pois bem... Este método não possui filtro nenhum. E nem instrução SELECT!!! Se executarmos ele diretamente, retornará todos os registros de perfil da tabela USR_PERFIL. Para criar filtros, criaremos um outro método, que receberá como parâmetros os valores com os quais restringiremos a nossa query, construímos todo o SQL necessário (instrução SELECT, adição de parâmetros, construção do where, etc) e colocaremos essa instrução no campo _SearchSQL. Feito isso, retornaremos o método ListaGenerica como o retorno do método filtrado, como no exemplo:
1: public List<TPerfilUsuario> Listar(string pDescricao)
2: {
3: _SearchSQL = "select * from USR_PERFIL where (1=1) ";
4: if (!pDescricao.Trim().Equals(""))
5: {
6: _SearchSQL += " and DESCRICAO like @DESC ";
7: AddSQLParam("@DESC", pDescricao.ToUpper().Trim() + "%", ParameterDirection.Input);
8: }
9: return ListaGenerica();
10: }
Tanto este método, como o de exclusão, transformaremos em métodos estáticos, conforme visto no código completo da classe.
Por último, temos o método que fará o trabalho sujo de ter que verificar se determinado perfil acessa tal módulo. Vamos a ele:
1: public bool TemAcesso(string pNomeForm)
2: {
3: bool r = false;
4: foreach (TPerfilModulo m in _Modulos)
5: {
6: if (m.PaginaWeb.ToUpper().Equals(pNomeForm.ToUpper()) && m.PodeAcessar.Equals(1))
7: {
8: r = true;
9: break;
10: }
11: }
12:
13: return r;
14: }
Ele recebe como parâmetro o nome do formulário apenas, SEM a URL completa e SEM a extensão .aspx. O retorno do método é inicializado como false, em seguida a coleção Modulos é varrida. Caso ele ache alguma correspondência do nome do formulário da lista com o parâmetro que informamos, e seu flag PodeAcessar for igual a 1, a variável tem seu valor mudado para true e a varredura é abortada. Note que para usá-lo, um objeto da classe já deve ter um perfil setado (método SetByID).
Ufa, terminou! Mas não a nossa biblioteca de classes. No próximo artigo iremos definir a classe de Usuários, aí sim a nossa biblioteca estará concluída :-)
Download:
Exemplo Sistema de Login em ASP.NET (com BD Firebird) (289 kB)
Um abraço a todos!
[Update 26/02/2008: Para facilitar o download, ao invés da página de suporte hospedada no Geocities, estarei movendo os arquivos para hospedagem própria, diretamente no domínio leonelfraga.com e colocando os links diretos para o arquivo.]
0 comentários:
Postar um comentário