1 - Introdução
Pra começar, o Jaas é uma implementação do padrão de segurança do j2ee.
Serve para controlar permissões de vários tipos: arquivos, diretórios, conteúdos, url’s…
Para web, basta seguir os passos abaixo.
Quando usamos este padrão de segurança,
devemos estar cientes que este modulo está a nível de servidor de aplicação e não de aplicação,
ou seja, este modulo de autenticação será executado pelo servidor de aplicação,
antes mesmo de acessar a aplicação.
2 - Criação do modulo de login
Existe uma interface definida no j2ee que é usada para efetuar o login.
javax.security.auth.spi.LoginModule
Devemos implementá-la, conforme o exemplo abaixo.
package br.com.guj.security.principals;
import java.security.Principal;
import java.util.Set;
/**
* @author fabio.viana
*/
public class User implements Principal{
private String name;
private Set roles;
public User(String name){
this.name = name;
}
public String getName() {
return name;
}
public Set getRoles() {
return roles;
}
public void setRoles(Set roles) {
if (this.roles == null)
this.roles = roles;
}
}
package br.com.guj.security.principals;
import java.security.Principal;
/**
* @author fabio.viana
*/
public class Role implements Principal{
private String name;
public Role(String name){
this.name = name;
}
public String getName() {
return name;
}
}
package br.com.guj.security;
import java.sql.*;
import java.util.*;
import javax.naming.*;
import javax.security.auth.*;
import javax.security.auth.callback.*;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;
import br.com.guj.security.principals.*;
/**
* @author fabio.viana
*/
public class GujLoginModule implements LoginModule {
private boolean commitSucceeded = false;
private boolean succeeded = false;
private User user;
private Set roles = new HashSet();
protected Subject subject;
protected CallbackHandler callbackHandler;
protected Map sharedState;
private String dataSourceName;
private String sqlUser;
private String sqlRoles;
public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) {
this.subject = subject;
this.callbackHandler = callbackHandler;
this.sharedState = sharedState;
dataSourceName = (String) options.get("dataSourceName");
sqlUser = (String) options.get("sqlUser");
sqlRoles = (String) options.get("sqlRoles");
}
public boolean login() throws LoginException {
// recupera o login e senha informados no form
getUsernamePassword();
Connection conn = null;
try {
// obtem a conexão
try {
Context initContext = new InitialContext();
Context envContext = (Context) initContext.lookup("java:/comp/env");
DataSource ds = (DataSource) envContext.lookup(dataSourceName);
conn = ds.getConnection();
} catch (NamingException e) {
succeeded = false;
throw new LoginException("Erro ao recuperar DataSource: " + e.getClass().getName() + ": " + e.getMessage());
} catch (SQLException e) {
succeeded = false;
throw new LoginException("Erro ao obter conexão: " + e.getClass().getName() + ": " + e.getMessage());
}
// valida o usuario
validaUsuario(conn);
} finally {
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
}
}
}
// acidiona o usuario e roles no mapa de compartilhamento
sharedState.put("javax.security.auth.principal", user);
sharedState.put("javax.security.auth.roles", roles);
return true;
}
public boolean commit() throws LoginException {
// adiciona o usuario no principals
if (user != null && !subject.getPrincipals().contains(user)) {
subject.getPrincipals().add(user);
}
// adiciona as roles no principals
if (roles != null) {
Iterator it = roles.iterator();
while (it.hasNext()) {
Role role = (Role) it.next();
if (!subject.getPrincipals().contains(role)) {
subject.getPrincipals().add(role);
}
}
}
commitSucceeded = true;
return true;
}
public boolean abort() throws LoginException {
if (!succeeded) {
return false;
} else if (succeeded && !commitSucceeded) {
succeeded = false;
} else {
succeeded = false;
logout();
}
this.subject = null;
this.callbackHandler = null;
this.sharedState = null;
this.roles = new HashSet();
return succeeded;
}
public boolean logout() throws LoginException {
// remove o usuario e as roles do principals
subject.getPrincipals().removeAll(roles);
subject.getPrincipals().remove(user);
return true;
}
/**
* Valida login e senha no banco
*/
private void validaUsuario(Connection conn) throws LoginException {
String senhaBanco = null;
PreparedStatement statement = null;
ResultSet rs = null;
try {
statement = conn.prepareStatement(sqlUser);
statement.setString(1, loginInformado);
rs = statement.executeQuery();
if (rs.next()) {
senhaBanco = rs.getString(1);
} else {
succeeded = false;
throw new LoginException("Usuário não localizado.");
}
} catch (SQLException e) {
succeeded = false;
throw new LoginException("Erro ao abrir sessão: "
+ e.getClass().getName() + ": " + e.getMessage());
} finally {
try {
if (rs != null)
rs.close();
if (statement != null)
statement.close();
} catch (Exception e) {
}
}
if (senhaInformado.equals(senhaBanco)) {
user = new User(loginInformado);
recuperaRoles(conn);
user.setRoles(roles);
return;
} else {
throw new LoginException("Senha Inválida.");
}
}
/**
* Recupera as roles no banco
*/
public void recuperaRoles(Connection conn) throws LoginException {
PreparedStatement statement = null;
ResultSet rs = null;
try {
statement = conn.prepareStatement(sqlRoles);
statement.setString(1, loginInformado);
rs = statement.executeQuery();
while (rs.next()) {
roles.add(new Role(rs.getString(1)));
}
roles.add(new Role("LOGADO"));
} catch (SQLException e) {
succeeded = false;
throw new LoginException("Erro ao recuperar roles: " + e.getClass().getName() + ": " + e.getMessage());
} finally {
try {
if (rs != null)
rs.close();
if (statement != null)
statement.close();
} catch (Exception e) {
}
}
}
/**
* Login do usuário.
*/
protected String loginInformado;
/**
* Senha do usuário.
*/
protected String senhaInformado;
/**
* Obtem o login e senha digitados
*/
protected void getUsernamePassword() throws LoginException {
if (callbackHandler == null)
throw new LoginException("Error: no CallbackHandler available to garner authentication information from the user");
Callback[] callbacks = new Callback[2];
callbacks[0] = new NameCallback("Login");
callbacks[1] = new PasswordCallback("Senha", false);
try {
callbackHandler.handle(callbacks);
loginInformado = ((NameCallback) callbacks[0]).getName();
char[] tmpPassword = ((PasswordCallback) callbacks[1]).getPassword();
senhaInformado = new String(tmpPassword);
((PasswordCallback) callbacks[1]).clearPassword();
} catch (java.io.IOException ioe) {
throw new LoginException(ioe.toString());
} catch (UnsupportedCallbackException uce) {
throw new LoginException("Error: " + uce.getCallback().toString() + " not available to garner authentication information from the user");
}
}
}
O método initialize() é invocado sempre que uma nova autenticação é solicitada.
São passados como parametros o Subject, CallbackHandler, o mapa de objetos compartilhados
e o mapa de opções do login.config (sessão 3). O mais importante destes parametros para nós será o options,
pois nele receberemos os parametros de sql e datasource.
O método login() é invocado quando é enviado os dados (login e senha) de autenticação.
O método commit() é invocado quando o login() obtém sucesso.
O método abort() é invocado quando o login() não obtém sucesso.
O método logout() é invocado quando o usuário desloga da aplicação.
Agora, com as classes de login criadas, devemos gerar um jar e adicionar no classpath do servidor de aplicação.
No tomcat, basta colocar o jar em $CATALINA_HOME/common/lib.
Obs.: Principal é a interface usada para acessar o login (no caso da classe User) e a role (no caso da classe Role)
3- Configurações
Devemos agora configurar o nosso login.
Para que o servidor de aplicação use nosso modulo de login, devemos criar um arquivo (login.config) contendo as informações necessárias para carregar o modulo.
Ele deve ter o seguinte formato:
NOME_DO_MODULO{
CLASSE_DE_LOGIN_1 (requerid) (parametro1=valor1..parametroN=valorN);
CLASSE_DE_LOGIN_2 (requerid) (parametro1=valor1..parametroN=valorN);
}
O flag requerid, indica se a autenticação naquela classe é requerida.
Podemos ter vários validadores de login (classe LoginModule) para o mesmo modulo de login.
Caso queira-se autenticar no banco e no ldap, basta implementar o LoginModule pra cada um e colocar no login.config.
No nosso exemplo ficaria assim:
guj {
br.com.guj.security.GujLoginModule required
dataSourceName="jdbc/GUJ"
sqlUser="select senha from tb_usuario where login=?"
sqlRoles="select id_role from tb_usuario_roles where login=?"
;
};
Apartir de agora, toda vez que iniciarmos o servidor de aplicação, será necessário informar o arquivo de configuração, desta forma:
-Djava.security.auth.login.config=$CATALINA_HOME/conf/login.config
Até aqui, o servidor de aplicação já estará configurado com nosso modulo.
4- Segurança na aplicação
Agora temos que adicionar a segurança em nossa aplicação.
Para que nossa aplicação faça uso do modulo de segurança, devemos adicionar um realm no contexto.
<Realm className="org.apache.catalina.realm.JAASRealm"
appName="guj"
userClassNames="br.com.guj.security.principals.User"
roleClassNames="br.com.guj.security.principals.Role"
debug="99"/>
Este parametro appName se refere ao NOME_DO_MODULO.
Agora basta adicionar as restrições da nossa aplicação. Isto é feito no web.xml. Veja abaixo:
...
<!-- Restrições -->
<security-constraint>
<display-name>cadastro de cientes</display-name>
<web-resource-collection>
<web-resource-name>cadastro de cientes</web-resource-name>
<url-pattern>/cliente.do</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>ADM</role-name>
<role-name>CAD_CLIENTE</role-name>
</auth-constraint>
</security-constraint>
<security-constraint>
<display-name>help da app</display-name>
<web-resource-collection>
<web-resource-name>help da app</web-resource-name>
<url-pattern>/help.do</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>LOGADO</role-name>
</auth-constraint>
</security-constraint>
...
<!-- Lista de Roles -->
<security-role>
<description>Quando usuario estiver logado</description>
<role-name>LOGADO</role-name>
</security-role>
<security-role>
<description>Administrador, pode fazer tudo</description>
<role-name>ADM</role-name>
</security-role>
<security-role>
<description>Para cadastrar cliente, deve se ter esta role</description>
<role-name>CAD_CLIENTE</role-name>
</security-role>
Repare que a url /cliente.do possue duas roles, será avaliado se o usuario posue alguma delas.
Repare tambem que foi criado uma role LOGADO. Ela serve pra validar aquelas url’s que basta que o usuario esteje logado.
Agora só falta criar o formulário de login.
Crie um arquivo para o formulario (login.jsp). Observe abaixo o exemplo.
<form method="POST" action="<%=request.getContextPath()%>/j_security_check">
Usuário: <input type="text" name="j_username" size="15"><br>
Senha: <input type="password" name="j_password" maxlength="20" size="15">
</form>
Sempre coloque os nomes dos campos de login e senha como j_username e j_password.
Este j_security_check é apenas para que o servidor de aplicação identifique que se trata de uma tentativa de login.
Crie tambem um arquivo de erro (erro.jsp).
Usuario ou senha inválido(s).<br>
<form method="POST" action="<%=request.getContextPath()%>/j_security_check">
Usuário: <input type="text" name="j_username" size="15"><br>
Senha: <input type="password" name="j_password" maxlength="20" size="15">
</form>
Para concluir, adicione mais esta configurão no web.xml. Trata-se do mapeamento dos arquivos login.jsp e erro.jsp
<login-config>
<auth-method>FORM</auth-method>
<realm-name>default</realm-name>
<form-login-config>
<form-login-page>/login.jsp</form-login-page>
<form-error-page>/erro.jsp</form-error-page>
</form-login-config>
</login-config>
Pronto!
Quando for acessado a url /cliente.do, o servidor de aplicação irá identificar que é necessário autenticação,
se não foi feita anteriromente ele irá redirecionar para a pagina de login até que o usuario seja autenticado.
5- Conclusão
O jaas é um pouco limitado e cheio de restrições, mas para controlar segurança e autenticação é o mais recomendado.
Caso precise de adicionar regras na autenticação, como por exemplo expiração de senha,
use um filter só para ver se o usuario está com a senha expirada.
Caso precise do usuario autenticado e/ou a(s) role(s) em um Servlet, Action… basta usar os metodos:
HttpServletRequest.getUserPrincipal(); // retorna o User
HttpServletRequest.isUserInRole("ADM"); // retorna se o usuario possue a role informada
User user = (User)HttpServletRequest.getUserPrincipal();
user.getRoles(); // roles do usuario
Caso tenham duvidas entrem em contato.