quinta-feira, 3 de maio de 2007

Implementando um sistema de login com JAAS no Tomcat

O que é?
O JAAS (Java Authentication and Authorization Service) é uma API para login e controle de acesso de usuários.

Proposta
Implementar um sistema de autenticação personalizado, pois os oferecidos não se adequavam a proposta inicial do sistema.

Problemas Encontrados
Como fazer a comunicação entre o sistema, que está rodando na web com o módulo de login, que está rodando por baixo do Tomcat.

Implementação
O módulo de login foi organizando com a seguinte estrutura:
  • imagemfilmes.auth -> Pacote que contém meu módulo de login
  • imagemfilmes.auth.obj -> Pacote que contém meus objetos auxiliares
  • imagemfilmes.auth.principals -> Pacote que contém as classes extendidas de Principals
IFLoginModule.java
/**
* Modulo de login da ImagemFilmes.
* @author Diego Fincatto
* @since 1.0
*/
public class IFLoginModule implements LoginModule {

private boolean commitSucedido = false;
private boolean operacaoSucedida = false;

private User usuario;
private Connection connection;
private Banco banco;

protected Subject subject;
protected CallbackHandler callbackHandler;
protected Map sharedState;


/**
* Metodo que faz a leitura dos parametros passados.
* @param subject Subject.
* @param callbackHandler CallbackHandler.
* @param sharedState SharedState.
* @param options Mapa com as opcoes definidas no login.conf.
*/
public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) {
this.subject = subject;
this.callbackHandler = callbackHandler;
this.sharedState = sharedState;

//cria um novo banco
Banco banco = new Banco();
banco.setSqlUser((String) options.get("sqlUser"));
banco.setSqlRoles((String) options.get("sqlRoles"));
banco.setBdDriver((String) options.get("bdDriver"));
banco.setBdUser((String) options.get("bdUser"));
banco.setBdPass((String) options.get("bdPass"));
banco.setBdURL((String) options.get("bdURL"));
this.setBanco(banco);
}

/**
* Metodo que efetua o login do usuario.
* @throws javax.security.auth.login.LoginException Caso nao pode efetuar o
* login.
* @return true.
*/
public boolean login() throws LoginException {
Connection conn = null;
try {
conn = this.getConnection();

// recupera o login e senha informados no form
Usuario usrCallBack = this.buscaUsuarioCallback(this.callbackHandler);

// valida o usuario
this.usuario = this.validaUsuario(conn, banco, usrCallBack);
} catch (ClassNotFoundException cnfe) {
this.operacaoSucedida = false;
throw new LoginException("Erro ao conectar com o banco: " + cnfe.getMessage());
} catch (SQLException e) {
this.operacaoSucedida = false;
throw new LoginException("Erro ao obter conexao: " + e.getClass().getName() + ": " + e.getMessage());
} finally {
try {conn.close();} catch (SQLException e) {}
}

// acidiona o usuario e roles no mapa de compartilhamento
this.sharedState.put("javax.security.auth.principal", this.usuario);
this.sharedState.put("javax.security.auth.roles", this.usuario.getRoles());

//remove mensagem de erro
this.setMensagem(this.usuario.getName(), "");

//retorna ok
return true;
}

/**
* Metodo executado depois das funcoes de login e logout.
* @throws javax.security.auth.login.LoginException Caso erro.
* @return true.
*/
public boolean commit() throws LoginException {
// adiciona o usuario no principals
if (this.usuario != null && !subject.getPrincipals().contains(this.usuario)) {
subject.getPrincipals().add(this.usuario);
}

// adiciona as roles no principals
if ( (this.usuario!=null) && (this.usuario.getRoles() != null) ){
for(Role role : this.usuario.getRoles()){
if (!subject.getPrincipals().contains(role)) {
subject.getPrincipals().add(role);
}
}
}

//seta o commit como feito
this.commitSucedido = true;

//retorna ok
return true;
}

/**
* Aborta alguma operacao.
* @throws javax.security.auth.login.LoginException Caso erro.
* @return true.
*/
public boolean abort() throws LoginException {
if (!this.operacaoSucedida) {
return false;
} else if (this.operacaoSucedida && !this.commitSucedido) {
this.operacaoSucedida = false;
} else {
this.operacaoSucedida = false;
this.logout();
}

//limpa os dados da autenticacao
this.subject = null;
this.callbackHandler = null;
this.sharedState = null;
this.usuario.setRoles(new HashSet());

//retorna o estado da operacao
return this.operacaoSucedida;
}

/**
* Metodo que executa logout no sistema.
* @throws javax.security.auth.login.LoginException Caso erro.
* @return true.
*/
public boolean logout() throws LoginException {
// remove o usuario e as roles do principals
subject.getPrincipals().clear();

//retorna ok
return true;
}



/**
* Este eh o metodo responsavel por validar o usuario no logon.
* Se conseguir validar, ja busca as Roles associadas a ele.
* @param conn Conexao com banco.
* @param _usuario Usuario a ser validado.
* @throws javax.security.auth.login.LoginException Caso nao consiga validar
* o usuário.
* @return true.
*/
private User validaUsuario(Connection conn, Banco _banco, Usuario _usuario) throws LoginException {
User retValue = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
pstmt = conn.prepareStatement(_banco.getSqlUser());
pstmt.setString(1, _usuario.getLogin());

//executa a query
rs = pstmt.executeQuery();

//verifica se o usuario existe na tabela
if (rs.next()) {
String senhaBanco = rs.getString(1);

//verifica se as senhas batem
if (senhaBanco.equals(this.geraMD5(_usuario.getSenha()))){
retValue = new User(_usuario.getLogin());
retValue.setRoles(this.recuperaRoles(conn, _banco, _usuario));
} else {
this.setMensagem(_usuario.getLogin(), "Senha Invalida!");
throw new LoginException("Senha Invalida!");
}
} else {
this.operacaoSucedida = false;
this.setMensagem(_usuario.getLogin(), "Usuario nao localizado!");
throw new LoginException("Usuario nao localizado!");
}

//atualiza o numero de logins efetuados
StringBuilder sql = new StringBuilder();
sql.append("UPDATE usuario_site ");
sql.append("SET acessonum = acessonum+1 ");
sql.append("WHERE email = ? ");

pstmt = conn.prepareStatement(sql.toString());
pstmt.setString(1, _usuario.getLogin());
pstmt.executeUpdate();

//retorna o usuario
return retValue;
} catch (SQLException e) {
this.operacaoSucedida = false;
throw new LoginException("Erro de SQL");
} finally {
try {rs.close();} catch (Exception e) {}
try {pstmt.close();} catch (Exception e) {}
}
}

/**
* Metodo que recupera as roles do usuario.
*
* @return true.
* @param _banco Banco a ser utilizado.
* @param conn Conexao com o banco.
* @param _usuario Usuario a ser pesquisado.
* @throws javax.security.auth.login.LoginException Caso erro.
*/
public Set recuperaRoles(Connection conn, Banco _banco, Usuario _usuario) throws LoginException {
Set retValue = new HashSet();
PreparedStatement statement = null;
ResultSet rs = null;
try {
statement = conn.prepareStatement(_banco.getSqlRoles());
statement.setString(1, _usuario.getLogin());
rs = statement.executeQuery();

//percorre os resultados
while (rs.next()) {
retValue.add(new Role(rs.getString(1)));
}

//adiciona roles estaticas
} catch (SQLException e) {
this.operacaoSucedida = false;
this.setMensagem(_usuario.getLogin(), "Erro ao recuperar roles!");
throw new LoginException("Erro ao recuperar roles!");
} finally {
try {rs.close();} catch (Exception e) {}
try {statement.close();} catch (Exception e) {}
}

//retorna
return retValue;
}

/**
* Este eh o metodo que pega o login e senha informados, independente do
* metodo usado para a autenticacao.
*
* @param _callback Callback de onde virao as informacoes.
* @throws javax.security.auth.login.LoginException Caso erro.
* @return Usuario.
*/
protected Usuario buscaUsuarioCallback(CallbackHandler _callback) throws LoginException {
Usuario retValue = null;
if (_callback == null){
throw new LoginException("Nao foi encontrado um CallbackHandler!");
}

//cria um array de calback para pegar nome de usuario e senha
Callback[] callbacks = new Callback[2];
callbacks[0] = new NameCallback("Login");
callbacks[1] = new PasswordCallback("Senha", false);

try {
//pega o callback
_callback.handle(callbacks);
//cria um usuario
retValue = new Usuario();
retValue.setLogin(((NameCallback) callbacks[0]).getName());
retValue.setSenha(new String(((PasswordCallback) callbacks[1]).getPassword()));

//limpa a senha
((PasswordCallback) callbacks[1]).clearPassword();
} catch (java.io.IOException ioe) {
throw new LoginException(ioe.toString());
} catch (UnsupportedCallbackException uce) {
throw new LoginException(uce.getCallback().toString() + " nao disponivel!");
}

//retorna o usuario
return retValue;
}
/**
* Funcao que gera MD5
* @param _str String que queremos gerar a md5
* @return retorna o md5 do parametro passado
*/
public static String geraMD5(String _str){
String retValue = "";
try{
//cria uma instancia de Messagedigest
MessageDigest md5 = MessageDigest.getInstance("MD5");
//gera md5
md5.update(_str.getBytes());
byte[] hash = md5.digest();
StringBuffer hexString = new StringBuffer();
for (int i = 0; i < retvalue =" hexString.toString();" connection ="="" connection =" DriverManager.getConnection(this.getBanco().getBdURL()," banco =" banco;">

Banco.java
**
* Classe que carrega as informacoes do banco de dados.
* @author Diego Fincatto
* @since 1.0
*/
public class Banco {

private String sqlUser;
private String sqlRoles;
private String bdDriver;
private String bdUser;
private String bdPass ;
private String bdURL;

/** Creates a new instance of Banco */
public Banco() {
this.setSqlUser("");
this.setSqlRoles("");
this.setBdDriver("");
this.setBdUser("");
this.setBdPass("");
this.setBdURL("");
}

public String getSqlUser() {
return sqlUser;
}

public void setSqlUser(String sqlUser) {
this.sqlUser = sqlUser;
}

public String getSqlRoles() {
return sqlRoles;
}

public void setSqlRoles(String sqlRoles) {
this.sqlRoles = sqlRoles;
}

public String getBdDriver() {
return bdDriver;
}

public void setBdDriver(String bdDriver) {
this.bdDriver = bdDriver;
}

public String getBdUser() {
return bdUser;
}

public void setBdUser(String bdUser) {
this.bdUser = bdUser;
}

public String getBdPass() {
return bdPass;
}

public void setBdPass(String bdPass) {
this.bdPass = bdPass;
}

public String getBdURL() {
return bdURL;
}

public void setBdURL(String bdURL) {
this.bdURL = bdURL;
}

}
Usuario.java
/**
* Classe que representa um usuario.
* @author Diego Fincatto
* @since 1.0
*/
public class Usuario {
private String login;
private String senha;

/** Creates a new instance of Usuario */
public Usuario() {
this.setLogin("");
this.setSenha("");
}

public String getLogin() {
return login;
}

public void setLogin(String login) {
this.login = login;
}

public String getSenha() {
return senha;
}

public void setSenha(String senha) {
this.senha = senha;
}

}
Role.java
/**
* Classe que representa uma role.
* @author Diego Fincatto
* @since 1.0
*/
public class Role implements Principal{

private String name;

public Role(String name){
this.name = name;
}

public String getName() {
return name;
}
}
User.java
/**
* Classe que representa um usuario.
* @author Diego Fincatto
* @since 1.0
*/
public class User implements Principal {
private String name;
private Set roles;

public User(String name){
this.setName(name);
}

private void setName(String name) {
this.name = name;
}

public String getName() {
return name;
}

public Set getRoles() {
return roles;
}

public void setRoles(Set roles) {
this.roles = roles;
}
}






Configurações Necessárias
Primeiramente, precisamos criar um arquivo onde dizemos para a API de autenticação do Java que dispomos de um sistema de login personalizado. O mesmo é feito criando o seguinte arquivo, chamado login.config:
IF {
imagemfilmes.auth.IFLoginModule required
dataSourceName="imagem"
sqlUser="select senha from usuario where login=? and ativo = true"
sqlRoles="select role from usuario_roles where login=?"
bdDriver="org.postgresql.Driver"
bdUser="usuario"
bdPass="senha"
bdURL="jdbc:postgresql://ipdoservidor/banco"
;
};
Explicando rapidamente as opções do arquivo, o IF é o "nome" do modulo. imagemfilmes.auth.IFLoginModule são, respectivamente, o nome do pacote e da classe que fará a autenticação. Daí pra baixo são opções que serão passadas para o modulo de login para podermos trabalhar la dentro, como veremos mais adiante. Nada disso é necessário, embora seja interessante usar, pois, caso algum desses parametros mude, é só mudar as configurações desses arquivo e tudo volta a funcionar. Caso o desenvolvedor optasse por fixar dentro do código, a cada mudança seria necessário uma nova compilação e um novo deploy do arquivo.
OBS: Percebam que implementei no PostgreSQL.

Após a criação do arquivo, devemos dizer ao java que ele existe (pois o java não é pai de santo pra adivinhar né?). Fazemos isso editando o arquivo $JAVA_HOME/jre/lib/security/java.security. Adicionamos o nosso modulo de login ao arquivo de configuração, como na linha que segue login.config.url.1=file:/lugarondevocecriouoarquivo/login.config.

E para que a aplicação possa usar o modulo, dizemos pra ela que o faça, modificando seu contexto como segue:


Basicamente, adicionamos um Realm ao contexto, informando o tipo (classname), o nome da aplicacao, definido lá no login.config, e as classes de User e Role que foram implementadas (Vide capitulo da implementacao).

Socorro
Alguma coisa não está saindo como o esperado? Não se desespere!!! Para tudo nessa vida há alguma solução(embora na maioria das vezes nós não saibamos qual é a merda da solução!!!).
Você poderá debugar o seu módulo iniciando seu tomcat com os seguintes parametros:
-Xdebug -Xrunjdwp:transport=dt_socket,address=8000,server=y,suspend=n

Finalizando
E é isso pessoal, muita coisa pode ser feita para que se ajuste as nossas necessidades. O JAAS é uma API excelente, embora poucos desenvolvedores a usam (pelo menos como deveriam).

Créditos
Abaixo cito as fontes onde busquei inspiração para a resolução dos problemas de percurso encontrados.
  1. http://java.sun.com/products/jaas/
  2. http://www.guj.com.br/posts/list/38988.java

Nenhum comentário: