quarta-feira, 13 de abril de 2011

Exemplo de utilização do framework Apache SHIRO para segurança

Este mês iniciei o desenvolvimento de um projeto web de complexidade média. Para este projeto o lema principal é: simplicidade e agilidade no desenvolvimento.

Na minha opinião, construir uma aplicação web JEE não é tarefa tão simples. Geralmente alguns frameworks precisam ser configurados e devidamente integrados. Para um projeto parecido, cheguei a utilizar JSF, Spring, Spring-Security e Hibernate-JPA.

Para o projeto atual, eu quis minimizar a utilização de alguns frameworks pois, considerando o contexto deste sistema, utilizar tudo isso seria uma prática de "over-engineering" (ou tentar usar uma bazooka para matar uma mosca).

Uma das coisas que fiz foi ao invés de usar Spring-security, usar o Apache SHIRO (AS).
No próprio site do projeto AS, encontrei a seguinte descrição na página principal:

Apache Shiro is a powerful and easy-to-use Java security framework that performs authentication, authorization, cryptography, and session management. With Shiro’s easy-to-understand API, you can quickly and easily secure any application – from the smallest mobile applications to the largest web and enterprise applications.

Bom, como o que eu procurei para este projeto foi simplicidade decidi testar este framework e o mesmo atendeu minhas espectativas.

Pelo que percebi, o nível de complexidade do Apache SHIRO pode aumentar de acordo com o número de customizações. Resumindo, achei o SHIRO mais simples do que o Spring-Framework (Não estou fazendo comparação de qual framework é melhor do que outro - cada caso é um caso).

Uma coisa legal do SHIRO é que pode-se utilizar o mesmo em aplicações desktop utilizando o conceito de Sessão assim como em aplicações web (Não sei se o spring-security oferece suporte direto a este tipo de funcionalidade).

Para iniciar a utilização do framework decidi estudar o básico do mesmo e criar uma famosa prova de conceito. No site do projeto é possível baixar um pacote com vários exemplos de utilização (É bom instalar o Maven).

Basicamente precisei ler o 10 Minute Tutorial e Understanding Realms in Apache Shiro.

No primeiro tutorial de 10 minutos do site do projeto, um exemplo stand-alone é apresentado e o mesmo utiliza configurações realizadas em um arquivo .ini. Neste arquivo estão configurados usuários, senhas, roles e permissions.

Como na maioria dos projetos, eu não quero deixar minhas configurações em arquivo texto até porquê o cadastro de usuários e senhas deve ser dinâmico. Para isso implementei minha versão de Realm conectando no meu próprio banco de dados para obter dados de usuários, credenciais e etc.

Definição de Realm: Realm é um componente que pode acessar dados específicos de segurança da aplicação como usuários, roles e permissões.

Para facilitar as coisas não é necessário implementar um Realm do zero. Para isso, o framework disponibiliza uma classe abstrata AuthorizingRealm que pode ser estendida.

Bom, após esta pequena introdução, no resto do post vou mostrar um exemplo de utilização com banco de dados pois como já foi dito, no exemplo simples do site do projeto é utilizado um arquivo .ini.

Criei um banco de dados em mysql e uma tabela chamada users. (Este exemplo é bem simples, onde o intuito é apenas documentar a customização de um Realm).

Abaixo, veja a estrutura do projeto no eclipse.



Temos 3 pacotes aí: com.test com as classes de teste do SHIRO, com.test.db com as classes necessárias para conectar no banco de dados e consultar os dados de usuário, e o pacote com.test.entity que possui um simples bean User com os dados de usuário: email e senha.
Outra coisa interessante a se notar, é o pequeno número de dependências no projeto. Com apenas 5 arquivos .jar foi possível realizar o teste.



Como o framework sabe que deve usar meu Realm customizado que utiliza o banco de dados?

Isso pode ser configurado no arquivo .ini que defini como shiro.ini no diretório resources que precisei colocar no classpath da minha aplicacao. (Veja como colocar um diretório da sua aplicacao no classpath caso esteja utilizando o eclipse).


[main]
customMatcher = com.test.CustomCredentialsMatcher
myRealm = com.test.CustomAuthorizingRealm
myRealm.credentialsMatcher = $customMatcher


Agora vamos ao código que é o que mais interessa:


QuickStart.java - Classe que utiliza o realm para autenticacao e autorizacao

package com.test;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;

public class QuickStart {
public static void main(String[] args) {

// carregando o arquivo de configuracao
Factory factory = new IniSecurityManagerFactory("classpath:shiro.ini");

// recuperando o usuario corrente (no caso nao ha nenhum conectado na aplicacao)
SecurityUtils.setSecurityManager(factory.getInstance());
Subject currentUser = SecurityUtils.getSubject();

// teste utilizando sessions
// gerenciamento de sessao
// caso o shiro estiver rodando em um contexto web, o session recuperado sera um httpsession.
Session session = currentUser.getSession();
session.setAttribute("key", "value");
String value = (String) session.getAttribute("ksey");
if (value != null && value.equals("value")) {
System.out.println("Session key:" + value);
}

// verificando se o usuario esta autenticado
if (!currentUser.isAuthenticated()) {
// se nao estiver entao inicia o login
UsernamePasswordToken token = new UsernamePasswordToken("tofux", "pfffmmmm");
token.setRememberMe(true);

try {
////////////////////////////////////////////////////////////////////////////////////////////////
// é aqui que toda o processo de autenticacao e utilizacao do realm personalizado entra em acao.
currentUser.login(token);
System.out.println(currentUser.getPrincipal());

// verificando roles
if (currentUser.hasRole("admin")) {
System.out.println("you are admin");
} else {
System.out.println("you aren't admin");
}

// verificando permissoes
if ( currentUser.isPermitted( "lightsaber:weild" ) ) {
System.out.println("You may use a lightsaber ring. Use it wisely.");
} else {
System.out.println("Sorry, lightsaber rings are for schwartz masters only.");
}

currentUser.logout();

// quando um usuario nao consegue logar na aplicacao, diferentes exceptions podem ser lancadas
// conforme abaixo. Há exceptions para quando o usuario nao está cadastrado, exceptions para senha inválida e etc.
} catch (UnknownAccountException uae) {
System.out.println("username isn't in the system");
} catch (IncorrectCredentialsException ice) {
System.out.println("password didn't match");
} catch (LockedAccountException lae) {
System.out.println("account for that username is locked");
}

}

}
}


CustomAuthorizingRealm.java: Este é o Realm customizado
Repare o método getAccount. A consulta no banco de dados é realizada apenas com o login de usuário. A senha não é validada aqui. Para isto, o shiro utiliza o conceito de CredentialsMatcher que também pode ser customizado e plugado na aplicacao. Já há implementacoes fornececidas para facilitar a vida.


package com.test;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAccount;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

import com.test.db.DaoUtils;
import com.test.entity.User;

public class CustomAuthorizingRealm extends AuthorizingRealm {

protected SimpleAccount getAccount(String username) {
User user = DaoUtils.getDaoUser().getById(username);
SimpleAccount account = new SimpleAccount(user.getEmail(), user.getPassword(), getName());
return account;
}

@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String username = (String) getAvailablePrincipal(principalCollection);
return getAccount(username);
}

@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
return getAccount(upToken.getUsername());
}

}


CustomCredentialsMatcher.java: CredentialsMatcher customizado. Bem, neste caso nao é tão customizado assim pois não sobrescrevi método algum da classe mantendo o comportamento padrão.

package com.test;

import org.apache.shiro.authc.credential.SimpleCredentialsMatcher;

public class CustomCredentialsMatcher extends SimpleCredentialsMatcher {
}


Agora as classes do sub-pacote db.


ConnectionFactory.java: apenas disponibiliza uma conexão com o BD.

package com.test.db;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class ConnectionFactory {

public static Connection getConnection() {
Connection connection = null;

try {
Class.forName("com.mysql.jdbc.Driver");
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/arrumetudo2","root", "tibicuera");
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}

return connection;

}

}


DaoUtils.java: Apenas devolve uma instância de um Dao para a classe de usuários.

package com.test.db;

public class DaoUtils {
public static MysqlDaoUser getDaoUser() {
return new MysqlDaoUser(ConnectionFactory.getConnection());
}
}



MysqlDaoUser.java: dao para recuperar o usuário do banco de dados

package com.test.db;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import com.test.entity.User;

public class MysqlDaoUser {
private Connection connection;

public MysqlDaoUser(Connection c) { connection = c; }

public User getById(String login) {
User user = new User();

try {
PreparedStatement statement = connection.prepareStatement("select * from User where email = ?");
statement.setString(1, login);
ResultSet rs = statement.executeQuery();
if (rs.next()) {
user.setEmail(rs.getString("email"));
user.setPassword(rs.getString("password"));
return user;
}
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}

user.setEmail("aa");
user.setPassword("aa");
return user;
}
}


E por fim, a classe de usuário do sub-pacote entity
User.java: sem comentários...


package com.test.entity;

public class User {
private String email;
private String password;

public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}

}




Conclusão
Este é um exemplo completo para testar e iniciar a utilizacão do shiro com um Realm customizado e simples. O próximo passo é estudar a utilizacão do shiro em ambiente web e disponibilizar mais um tutorial no blog. Espero que com este simples exemplo, seja mais fácil customizar sem ter que ler um pouco mais sobre as API´s de realms e credentials matcher.



Referências
http://shiro.apache.org

Um comentário:

  1. Boa Tarde! Daora o post, ta me ajudando no meu TCC, to fazendo uns testes com login e estou com um problema, vc poderia me ajudar? Meu Netbeans 7.0 ta acusando erro aqui: Factory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
    ali no securitymanager, e ainda me sugere criar uma classe de nome securitymanager, e se eu mudar para a classe SecurityManager ele da erro e pede pra trocar o tipo do objeto para IniSecurityManagerFactory. Obrigado desde ja, espero nao estar atrapalhando :)

    ResponderExcluir