Controle de acesso com o Demoiselle 2.1
Na semana passada foi anunciada a versão 2.1 do framework Demoiselle. Dentre as novidades está o controle de acesso simples, fácil e flexível. Neste post explico como personalizar esta funcionalidade mostrando código funcionando. Veja como é fácil!
Se você quer controlar o acesso à sua aplicação, saiba que a versão 2.1 do Demoiselle vai facilitar muito a sua vida. No exemplo, criei uma aplicação através do arquétipo demoiselle-minimal (veja neste vídeo como fazer isto). O primeiro passo foi definir os pontos de controle. Utilizei anotações:
public class HelloWorld {
@Inject
private Logger logger;
@RequiredPermission(resource = "hello", operation = "say")
public void say() {
logger.info("Saying hello on console");
}
@RequiredRole("administrators")
public void swear() {
logger.info("I swear...");
}
}
Para executar o método say() é preciso ter permissão ao recurso “hello” com a ação “say”, conforme anotação @RequiredPermission. Para executar swear(), o usuário deve pertencer ao grupo de administradores. Só utilize a anotação @RequiredRole caso sua aplicação possua grupos fixos, senão dê preferência a @RequiredPermission. Se quer aprofundar neste assunto, sugiro a leitura do padrão RBAC.
O passo seguinte foi criar o autenticador e autorizador personalizados. Lembra dos conceitos de controle de acesso? Autenticar é identificar o usuário, autorizar é determinar o que pode ser acessado. Comecei pelo autenticador. Criei um objeto para transportar as credenciais do usuário, neste caso o login e senha, mas poderia ser qualquer outra coisa:
@SessionScoped
public class MyCredentials implements Serializable {
private String username;
private String password;
public void clear() {
username = null;
password = null;
}
// Getters and setters
}
Em seguida criei o autenticador, uma implementação da interface Authenticator do Demoiselle:
@Alternative
public class MyAuthenticator implements Authenticator {
@Inject
private MyCredentials credentials;
@Override
public boolean authenticate() {
String usr = credentials.getUsername();
String pwd = credentials.getPassword();
boolean authenticated = false;
if (usr.equals("admin") && pwd.equals("secret")) {
authenticated = true;
} else if (usr.equals("zyc") && pwd.equals("tcharam")) {
authenticated = true;
}
return authenticated;
}
@Override
public void unAuthenticate() {
credentials.clear();
}
@Override
public User getUser() {
return new User() {
@Override
public String getId() {
return credentials.getUsername();
}
@Override
public void setAttribute(Object key, Object value) {
}
@Override
public Object getAttribute(Object key) {
return null;
}
};
}
}
A implementação foi bem simples, mas poderia ler um XML, acessar banco de dados, conectar ao LDAP, consultar outro sistema, consumir serviços web ou delegar para frameworks especializados.
Ativei a estratégia de autenticação no /META-INF/beans.xml:
<beans> <alternatives> <class>examples.MyAuthenticator</class> </alternatives> </beans>
Para testar o funcionamento, criei um caso de teste com JUnit:
@RunWith(DemoiselleRunner.class)
public class HelloWorldTest {
@Inject
private SecurityContext context;
@Inject
private MyCredentials credentials;
@Test
public void loginSuccessful() {
// Acessando com meu usuário
credentials.setUsername("zyc");
credentials.setPassword("tcharam");
context.login();
assertEquals("zyc", context.getUser().getId());
context.logout();
// Acessando como admin
credentials.setUsername("admin");
credentials.setPassword("secret");
context.login();
assertEquals("admin", context.getUser().getId());
context.logout();
}
@Test
public void loginFailed() {
// Tentando acessar com usuário inexistente
credentials.setUsername("fake");
credentials.setPassword("idontknow");
context.login();
assertNull(context.getUser());
}
}
O método login() da interface SecurityContext delega a chamada para a estratégia de autenticação ativa. Todas as funcionalidades de segurança devem ser acessadas exclusivamente através de SecurityContext. Olha as funcionalidades quais são:
interface SecurityContext {
void login();
void logout() throws NotLoggedInException;
boolean isLoggedIn();
boolean hasPermission(String resource, String operation)
throws NotLoggedInException;
boolean hasRole(String role) throws NotLoggedInException;
User getUser();
}
Voltando ao que interessa… criei o autorizador, uma implementação da interface Authorizator do Demoiselle:
@Alternative
public class MyAuthorizator implements Authorizator {
@Inject
private SecurityContext context;
@Override
public boolean hasRole(String role) {
String usr = context.getUser().getId();
boolean authorized = false;
if (usr.equals("admin") &&
role.equals("administrators")) {
authorized = true;
}
return authorized;
}
@Override
public boolean hasPermission(Object res, String op) {
String usr = context.getUser().getId();
boolean authorized = false;
if (usr.equals("zyc") &&
res.equals("hello") && op.equals("say")) {
authorized = true;
} else if (context.hasRole("administrators")) {
authorized = true;
}
return authorized;
}
}
O método hasRole() só retorna verdade caso “admin” esteja autenticado. O hasPersmission() implementa uma regra específica para “zyc” e permite que integrantes do grupo de administradores tenham passe-livre. Estas regras poderiam estar implementadas de forma mais complexa, acessando um banco de dados, consumindo serviços web ou delegando para frameworks especializados.
Ativei a estratégia de autorização no /META-INF/beans.xml:
<beans> <alternatives> <class>examples.MyAuthenticator</class> <class>examples.MyAuthorizator</class> </alternatives> </beans>
Em seguida complementei o caso de teste:
@RunWith(DemoiselleRunner.class)
public class HelloWorldTest {
@Inject
private SecurityContext context;
@Inject
private MyCredentials credentials;
@Inject
private HelloWorld helloWorld;
@Test
public void loginSuccessful() {
// Acessando com meu usuário
credentials.setUsername("zyc");
credentials.setPassword("tcharam");
context.login();
assertEquals("zyc", context.getUser().getId());
context.logout();
// Acessando como admin
credentials.setUsername("admin");
credentials.setPassword("secret");
context.login();
assertEquals("admin", context.getUser().getId());
context.logout();
}
@Test
public void loginFailed() {
// Tentando acessar com usuário inexistente
credentials.setUsername("fake");
credentials.setPassword("idontknow");
context.login();
assertNull(context.getUser());
}
@Test
public void authorizedAccess() {
// Acessando o método "say" com meu usuário
credentials.setUsername("zyc");
credentials.setPassword("tcharam");
context.login();
helloWorld.say();
context.logout();
// Acessando o método "say" e "swear" como admin
credentials.setUsername("admin");
credentials.setPassword("secret");
context.login();
helloWorld.say();
helloWorld.swear();
context.logout();
}
@Test
public void unauthorizedAccess() {
// Tentando acessar o método "swear"
credentials.setUsername("zyc");
credentials.setPassword("tcharam");
context.login();
try {
helloWorld.swear();
fail();
} catch (SecurityException e) {
// Se chegar aqui, o teste deve passar
}
context.logout();
}
}
Tudo pronto e funcionando perfeitamente! O código-fonte produzido pode ser acessado publicamente através deste repositório, sinta-se à vontade para baixar, experimentar e modificar. A documentação de referência do Demoiselle pode ser acessada aqui.
Para facilitar ainda mais sua vida, disponibilizamos uma extensão para o Apache Shiro e estamos trabalhando na integração com o projeto Rasea. Em breve, mais novidades!
Filed under: Post | 4 Comments
Tags:Demoiselle, Software Livre







Fiquei com uma dúvida.
O método login da implementação de securityContext é do tipo void.
E quando eu chamo
ele vai para index como faço para ir para página que eu desejar?
//Metodo login do securityContext
@Override
public void login() {
if (config.isEnabled() && authenticator.get().authenticate()) {
Authenticator auth = authenticator.get();
user = auth.getUser();
loggedIn = true;
// Esse código abaixo redireciona para a index.
Beans.getBeanManager().fireEvent(new AfterLoginSuccessful() {
private static final long serialVersionUID = 1L;
});
}
}
Olá, dá uma olhada nesta thread da lista:
https://sourceforge.net/mailarchive/forum.php?thread_name=CAN8MHi-HyuogDwiif6J_mWUX3SR_kS%2BW0NnymLBU2U89T6oyxA%40mail.gmail.com&forum_name=demoiselle-users
Aproveita e se cadastra na lista
https://lists.sourceforge.net/lists/listinfo/demoiselle-users