|
|
Fábio Viana
Uma introdução a segurança em Java com JAAS
Tutorial de JAAS
1 - Introdução
O JAAS (Java Authentication and Authorization Service) é um conjunto que APIs que permite que as aplicações Java tenham um controle autenticação e de acesso. O JAAS implementa uma versão Java do framework padrão Pluggable Authentication Module (PAM), e suporta autorização baseada em usuário. Isso permite que aplicações fiquem independentes desse controle de segurança.
Serve para controlar permissões de vários tipos de recursos: arquivos, diretórios, conteúdos, URLs.
Para aplicações 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:
01 package br.com.guj.security.principals;
02 import java.security.Principal;
03 import java.util.Set;
04
05 /**
06 * @author fabio.viana
07 */
08 public class User implements Principal{
09 private String name;
10 private Set roles;
11
12 public User(String name){
13 this.name = name;
14 }
15
16 public String getName() {
17 return name;
18 }
19
20 public Set getRoles() {
21 return roles;
22 }
23
24 public void setRoles(Set roles) {
25 if (this.roles == null)
26 this.roles = roles;
27 }
28 }
|
01 package br.com.guj.security.principals;
02 import java.security.Principal;
03
04 /**
05 * @author fabio.viana
06 */
07 public class Role implements Principal{
08 private String name;
09
10 public Role(String name){
11 this.name = name;
12 }
13
14 public String getName() {
15 return name;
16 }
17 }
|
001 package br.com.guj.security;
002 import java.sql.*;
003 import java.util.*;
004 import javax.naming.*;
005 import javax.security.auth.*;
006 import javax.security.auth.callback.*;
007 import javax.security.auth.login.LoginException;
008 import javax.security.auth.spi.LoginModule;
009 import br.com.guj.security.principals.*;
010
011 /**
012 * @author fabio.viana
013 */
014 public class GujLoginModule implements LoginModule {
015 private boolean commitSucceeded = false;
016 private boolean succeeded = false;
017
018 private User user;
019 private Set roles = new HashSet();
020
021 protected Subject subject;
022 protected CallbackHandler callbackHandler;
023 protected Map sharedState;
024 private String dataSourceName;
025 private String sqlUser;
026 private String sqlRoles;
027
028 public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) {
029 this.subject = subject;
030 this.callbackHandler = callbackHandler;
031 this.sharedState = sharedState;
032 dataSourceName = (String) options.get("dataSourceName");
033 sqlUser = (String) options.get("sqlUser");
034 sqlRoles = (String) options.get("sqlRoles");
035 }
036
037 public boolean login() throws LoginException {
038 // recupera o login e senha informados no form
039 getUsernamePassword();
040
041 Connection conn = null;
042 try {
043 // obtem a conexão
044 try {
045 Context initContext = new InitialContext();
046 Context envContext = (Context) initContext.lookup("java:/comp/env");
047 DataSource ds = (DataSource) envContext.lookup(dataSourceName);
048 conn = ds.getConnection();
049 } catch (NamingException e) {
050 succeeded = false;
051 throw new LoginException("Erro ao recuperar DataSource: " + e.getClass().getName() + ": " + e.getMessage());
052 } catch (SQLException e) {
053 succeeded = false;
054 throw new LoginException("Erro ao obter conexão: " + e.getClass().getName() + ": " + e.getMessage());
055 }
056 // valida o usuario
057 validaUsuario(conn);
058 } finally {
059 if (conn != null) {
060 try {
061 conn.close();
062 } catch (SQLException e) {
063 }
064 }
065 }
066 // acidiona o usuario e roles no mapa de compartilhamento
067 sharedState.put("javax.security.auth.principal", user);
068 sharedState.put("javax.security.auth.roles", roles);
069
070 return true;
071 }
072
073 public boolean commit() throws LoginException {
074 // adiciona o usuario no principals
075 if (user != null && !subject.getPrincipals().contains(user)) {
076 subject.getPrincipals().add(user);
077 }
078 // adiciona as roles no principals
079 if (roles != null) {
080 Iterator it = roles.iterator();
081 while (it.hasNext()) {
082 Role role = (Role) it.next();
083 if (!subject.getPrincipals().contains(role)) {
084 subject.getPrincipals().add(role);
085 }
086 }
087 }
088
089 commitSucceeded = true;
090 return true;
091 }
092
093 public boolean abort() throws LoginException {
094 if (!succeeded) {
095 return false;
096 } else if (succeeded && !commitSucceeded) {
097 succeeded = false;
098 } else {
099 succeeded = false;
100 logout();
101 }
102
103 this.subject = null;
104 this.callbackHandler = null;
105 this.sharedState = null;
106 this.roles = new HashSet();
107
108 return succeeded;
109 }
110
111 public boolean logout() throws LoginException {
112 // remove o usuario e as roles do principals
113 subject.getPrincipals().removeAll(roles);
114 subject.getPrincipals().remove(user);
115 return true;
116 }
117
118 /**
119 * Valida login e senha no banco
120 */
121 private void validaUsuario(Connection conn) throws LoginException {
122 String senhaBanco = null;
123 PreparedStatement statement = null;
124 ResultSet rs = null;
125 try {
126 statement = conn.prepareStatement(sqlUser);
127 statement.setString(1, loginInformado);
128 rs = statement.executeQuery();
129 if (rs.next()) {
130 senhaBanco = rs.getString(1);
131 } else {
132 succeeded = false;
133 throw new LoginException("Usuário não localizado.");
134 }
135 } catch (SQLException e) {
136 succeeded = false;
137 throw new LoginException("Erro ao abrir sessão: "
138 + e.getClass().getName() + ": " + e.getMessage());
139 } finally {
140 try {
141 if (rs != null)
142 rs.close();
143 if (statement != null)
144 statement.close();
145 } catch (Exception e) {
146
147 }
148 }
149
150 if (senhaInformado.equals(senhaBanco)) {
151 user = new User(login);
152 recuperaRoles(conn);
153 user.setRoles(roles);
154 return;
155 } else {
156 throw new LoginException("Senha Inválida.");
157 }
158 }
159
160 /**
161 * Recupera as roles no banco
162 */
163 public void recuperaRoles(Connection conn) throws LoginException {
164 PreparedStatement statement = null;
165 ResultSet rs = null;
166 try {
167 statement = conn.prepareStatement(sqlRoles);
168 statement.setString(1, loginInformado);
169 rs = statement.executeQuery();
170 while (rs.next()) {
171 roles.add(new Role(rs.getString(1)));
172 }
173 roles.add(new Role("LOGADO"));
174 } catch (SQLException e) {
175 succeeded = false;
176 throw new LoginException("Erro ao recuperar roles: " + e.getClass().getName() + ": " + e.getMessage());
177 } finally {
178 try {
179 if (rs != null)
180 rs.close();
181 if (statement != null)
182 statement.close();
183 } catch (Exception e) {
184
185 }
186 }
187 }
188
189 /**
190 * Login do usuário.
191 */
192 protected String loginInformado;
193
194 /**
195 * Senha do usuário.
196 */
197 protected String senhaInformado;
198
199 /**
200 * Obtem o login e senha digitados
201 */
202 protected void getUsernamePassword() throws LoginException {
203 if (callbackHandler == null)
204 throw new LoginException("Error: no CallbackHandler available to garner authentication information from the user");
205
206 Callback[] callbacks = new Callback[2];
207 callbacks[0] = new NameCallback("Login");
208 callbacks[1] = new PasswordCallback("Senha", false);
209 try {
210 callbackHandler.handle(callbacks);
211 loginInformado = ((NameCallback) callbacks[0]).getName();
212 char[] tmpPassword = ((PasswordCallback) callbacks[1]).getPassword();
213 senhaInformado = new String(tmpPassword);
214 ((PasswordCallback) callbacks[1]).clearPassword();
215 } catch (java.io.IOException ioe) {
216 throw new LoginException(ioe.toString());
217 } catch (UnsupportedCallbackException uce) {
218 throw new LoginException("Error: " + uce.getCallback().toString() + " not available to garner authentication information from the user");
219 }
220 }
221 }
|
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:
1 NOME_DO_MODULO {
2 CLASSE_DE_LOGIN_1 (requerid) (parametro1=valor1..parametroN=valorN);
3 CLASSE_DE_LOGIN_2 (requerid) (parametro1=valor1..parametroN=valorN);
4 }
|
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:
1 guj {
2 br.com.guj.security.GujLoginModule required
3 dataSourceName="jdbc/GUJ"
4 sqlUser="select senha from tb_usuario where login=?"
5 sqlRoles="select id_role from tb_usuario_roles where login=?"
6 ;
7 };
|
A partir 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.
01 ...
02 <!-- Restrições -->
03 <security-constraint>
04 <display-name>cadastro de cientes</display-name>
05 <web-resource-collection>
06 <web-resource-name>cadastro de cientes</web-resource-name>
07 <url-pattern>/cliente.do</url-pattern>
08 </web-resource-collection>
09 <auth-constraint>
10 <role-name>ADM</role-name>
11 <role-name>CAD_CLIENTE</role-name>
12 </auth-constraint>
13 </security-constraint>
14
15 <security-constraint>
16 <display-name>help da app</display-name>
17 <web-resource-collection>
18 <web-resource-name>help da app</web-resource-name>
19 <url-pattern>/help.do</url-pattern>
20 </web-resource-collection>
21 <auth-constraint>
22 <role-name>LOGADO</role-name>
23 </auth-constraint>
24 </security-constraint>
25 ...
26 <!-- Lista de Roles -->
27 <security-role>
28 <description>Quando usuario estiver logado</description>
29 <role-name>LOGADO</role-name>
30 </security-role>
31
32 <security-role>
33 <description>Administrador, pode fazer tudo</description>
34 <role-name>ADM</role-name>
35 </security-role>
36
37 <security-role>
38 <description>Para cadastrar cliente, deve se ter esta role</description>
39 <role-name>CAD_CLIENTE</role-name>
40 </security-role>
41
|
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:
1 <form method="POST" action="<%=request.getContextPath()%>/j_security_check">
2 Usuário: <input type="text" name="j_username" size="15"><br>
3 Senha: <input type="password" name="j_password" maxlength="20" size="15">
4 </form>
5
|
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):
1 Usuario ou senha inválido(s).<br>
2 <form method="POST" action="<%=request.getContextPath()%>/j_security_check">
3 Usuário: <input type="text" name="j_username" size="15"><br>
4 Senha: <input type="password" name="j_password" maxlength="20" size="15">
5 </form>
6
|
Para concluir, adicione mais esta configurão no web.xml. Trata-se do mapeamento dos arquivos login.jsp e erro.jsp.
1 <login-config>
2 <auth-method>FORM</auth-method>
3 <realm-name>default</realm-name>
4 <form-login-config>
5 <form-login-page>/login.jsp</form-login-page>
6 <form-error-page>/erro.jsp</form-error-page>
7 </form-login-config>
8 </login-config>
9
|
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:
1
2 HttpServletRequest.getUserPrincipal(); // retorna o User
3 HttpServletRequest.isUserInRole("ADM"); // retorna se o usuario possue a role informada
4 User user = (User)HttpServletRequest.getUserPrincipal();
5 user.getRoles(); // roles do usuario
6
|
|
|
|