Tutorial de JAAS

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:&nbsp;<input type="text" name="j_username" size="15"><br>
	Senha:&nbsp;<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:&nbsp;<input type="text" name="j_username" size="15"><br>
	Senha:&nbsp;<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.

Porque você não escreveu um artigo no formato do guj?

é q não sei como fazer isso…

concordo no formato guj… iai fica loco…

tem meu voto

Apenas uma opinião pessoal. Vc não acha JAAS uma bazuka para matar uma mosca?

mate-a bem matado…
o bom do jaas é que vc pode programar suas actions, servlets, controllers… sem se preocupar com a segurança de acesso. basta só mapear as restrições.

[quote=saoj]
Apenas uma opinião pessoal. Vc não acha JAAS uma bazuka para matar uma mosca?[/quote]

Como vc faz sem os JAAS?

Eu estou utilizando proxy.

So estou perguntando pq ainda n~]ao vi nenhuma aplicação saudavel de segurança.

A segurança pode ser controlada por um filtro que intercepta actions e/ou páginas.

No caso das páginas tb pode ser feita por tags.

Dá uma olhada aqui:

http://www.mentaframework.org/authentication.jsp

http://www.mentaframework.org/authorization.jsp

prefiro o padrão j2ee para segurança, visto que posso usar este mesmo modulo em qualquer plataforma, web, desktop… com independencia, inclusive sobre outras frameworks, como spring, struts, mentawai, webwork… isto porque a segurança do jaas está a nivel de servidor de aplicação e nao restritamente à aplicação.

E no caso de não poder usar o Mentawai? Uma solução padrão que todos os app. servers devem (ou pelo menos deveriam) implementar pode ser muito útil.

[color="blue]Sim. Bem-vindo ao EJB![/color] Divirta-se.

[quote=saoj][quote]
Uma solução padrão que todos os app. servers devem (ou pelo menos deveriam) implementar pode ser muito útil.
[/quote]

[color="blue]Sim. Bem-vindo ao EJB![/color] Divirta-se.

[/quote]

Sergio,

Mas JAAS != EJB.

]['s

Mais um pro JAAS… Servidor que presta implementa SSO mas para isso tem q tah usando o JAAS… Por isso prefiro JAAS… E uma coisa que vc faz uma vez e eskece…

Tudo depois de feito fica fácil. De novo isso é uma questão de gosto pessoal. Eu particularmente achei o tutorial extenso, não por má-qualidade do tutorial, que por sinal está excelente pois entendi tudo, mas pela complexidade do JAAS.

E como ele suporta autorização de apenas um pedaço dentro da página? E como ele suporta redirecionamento depois do login? Deve suportar… Outro tutorial? :slight_smile:

Acho que a tendencia atual é ir contra tudo que não é simples. Se um conta-gotas mata a mosca então use o conta-gotas e não a bazuka. JAAS e EJB já tiveram seu momento. Bom… Minha humilde opinião… Vide a hype de RoR e tirem suas conclusões…

Conclusão retirada do livro ‘Java Development with Spring Framework’, na parte que fala sobre Acegi Security, logo depois de mostrar as vantagens do uso do JAAS:

“Certainly JAAS is an important standard when developing applications intended for a limited privilege execution environment, such as an applet. However, unless web and EJB container support for JAAS greatly improves, it is unlikely to prove practical for efficiently developing portable, secure enterprise applications.”

Quem não tem o livro, achei um pdf perdido no google, nesse endereço:
http://searchappsecurity.techtarget.com/searchAppSecurity/downloads/Spring_Framework_ch10.pdf

Para quem não conhece direito JAAS, é interessante de se ler, e principalmente saber quando se deve usar isso. Sabendo usar quando necessário, não há mal nenhum. O problema é usar para “matar uma mosca”, como já disseram… :smiley:

para fazer autorizações em pedações da pagina com jaas é só fazer assim:

<%
if (request.isUserInRole("admins") && request.isUserInRole("managers")){ 
%>
Only authorized people can see this.
<%
}
%>
<%
if (request.isUserInRole("create")){ 
%>
Only authorized people can see this.
<%
}
%>

mas se não quiser usar scriptlets, crie uma tagfile ou taglib.

Tudo bem, mas tem que fazer uma tag pra isso. Código no meio de JSP hoje em dia não dá mais para aceitar…

depois vou escrever um tutorial de taglib e tagfile…

Optem sempre pelo padrão JAAS, por várias razões:

  • padrão industrial para TODOS (atualmente +de 50) os AppServer Java Enterprise

  • não precisa ser homologado individualmente (algumas empresas não aceitam software OpenSource em ambientes produtivos)

  • aderente ao SSO usando atualmente no mercado

  • TOTALMENTE DESCRITIVO (não é necessário alterar código para mudar a relação usuário-grupo-permissão).

Excelente tópico.

obrigado!
virá mais… aguardem…