Artigo Java Magazine 69 - Spring Security
Aprenda a criar um mecanismo de autenticação e controle de acesso para sua aplicação web, de forma fácil e personalizável, superando em vários aspectos a segurança tradicional Java EE
Atenção: esse artigo tem um vídeo complementar. Clique e assista!
O artigo apresenta o projeto Spring Security como uma alternativa na área de segurança à tradicional especificação Java EE, através de um exemplo prático e realista.
Para que serve:
Com o Spring Security é possível criar um mecanismo de autenticação e autorização para sua aplicação web em questão de minutos. O framework foca em facilitar a implementação dos casos de uso mais freqüentes, porém oferece valiosos pontos de extensão para requisitos mais complexos. Por fim, disponibiliza suporte a inúmeros diferentes tipos de autenticação e integração com as mais usadas tecnologias na área de segurança.
Em que situação o tema é útil:
Para qualquer aplicação web que necessite restringir seus recursos para diferentes tipos de usuário, bem como assegurar que se autentiquem de forma prática e segura.
Spring Security:
O Spring Security surgiu da necessidade de melhorar o suporte à segurança oferecido pela especificação Java EE. O framework centraliza a configuração em um único XML, dispensando configurações do container e tornando a aplicação web um arquivo WAR auto contido.
Para começar a utilizá-lo basta adicionar seus JARs ao classpath, configurar um filtro e um listener no web.xml e criar um application context (XML de configuração). O XML centraliza todas as configurações de autenticação e autorização. As tags <intercept-url> definem quais roles podem acessar cada grupo de URLs. A tag <authentication-provider> define a fonte de dados para as informações de usuários (banco de dados, arquivo de propriedades, LDAP, etc.).
Quando necessário, é possível utilizar os eventos publicados pelo framework a cada sucesso ou falha na autenticação ou autorização. Ouvir os eventos permite criar complexos casos de gerenciamento de usuários. O Spring Security ainda oferece integrações com a API de Servlets, taglibs para facilitar a codificação de JSPs, suporte à HTTPS, segurança em métodos com uso de anotações e suporte a autenticação com LDAP ou certificados X509.
Segurança é um requisito importante presente na grande maioria dos sistemas desenvolvidos. Na plataforma Java EE temos uma solução oferecida pela especificação que determina como uma aplicação pode definir regras de controle de acesso e autenticação. Entretanto, ainda é comum nos depararmos com soluções “caseiras” para cumprir tal requisito. Em parte, tais soluções são criadas pela falta de experiência dos desenvolvedores, ou por outro lado, porque a especificação não é flexível o suficiente para comportar os requisitos.
Com o objetivo de preencher a lacuna deixada pela especificação, em 2003 surgiu o Acegi Security System for Spring. O Acegi Security é conhecido por ser extremamente configurável e poderoso, porém difícil de utilizar devido à enorme quantidade de configuração XML necessária. Em 2007 o projeto Acegi foi incorporado dentro do guarda-chuva de projetos do Spring Framework Portifolio, e então, renomeado como Spring Security. Em abril de 2008 a versão 2.0.0 do Spring Security foi lançada tomando como proveito a configuração baseada em namespaces do Spring 2.0. Hoje, o Spring Security é extremamente fácil de configurar, sem perder a flexibilidade e o poder do antigo Acegi.
O Spring Security depende de alguns JARs do Spring Framework “core”. Entretanto, não é necessário que sua aplicação seja construída com o modelo de programação do Spring Framework. Ou seja, uma aplicação pré-existente que não usa Spring pode passar a utilizar o Spring Security sem grandes modificações. Para aprender mais sobre o Spring Framework consulte o artigo de capa da Edição 65.
Assim como o Java EE o Spring Security possui uma abordagem declarativa para segurança, baseada em roles (papéis). A abordagem é declarativa, pois a aplicação não precisa chamar nenhum método para realizar autenticação ou autorização, tudo é feito através de configuração XML.
Para configurar o Spring Security de forma declarativa, assim como no Java EE, é necessário declarar quais serão os roles envolvidos, quais os recursos que serão protegidos, e quais roles podem acessar cada recurso. Além disso, declara-se como a autenticação será feita (basic, digest, form login, LDAP, etc.).
Este artigo apresenta as características do Spring Security, mostrando alguns recursos importantes não presentes na especificação Java EE. Exemplos práticos serão construídos, abordando cenários frequentes que são requisitos de grande parte das aplicações web.
Os conceitos básicos sobre segurança não são abordados no artigo. Entretanto, o artigo de capa da Edição 22 descreve tais conceitos e demonstra exemplos práticos utilizando a especificação Java EE.
Primeiro exemplo
Como primeiro exemplo vamos criar uma aplicação web simples com duas áreas de acesso restrito: uma permitida para qualquer usuário autenticado (/usuarios/index.jsp) e outra apenas para usuários administradores (/admin/index.jsp). As páginas restritas apenas exibem uma mensagem e possuem um link de retorno à página principal. Espera-se que um login seja solicitado ao acessar qualquer uma destas áreas. A página inicial da aplicação (/index.jsp), ilustrada na Figura 1, tem acesso livre e possui links para as duas áreas restritas.
Figura 1. Página inicial da aplicação.
O código fonte dos três JSPs são HTML simples e portanto não serão exibidos nas listagens (estão disponíveis para download no site da Java Magazine).
Configurando o web.xml
O Spring Security utiliza-se de um filtro HTTP, declarado no web.xml (Listagem 1), para interceptar todas as URLs acessadas e conferir suas permissões de acesso. Por isso, o filtro é aplicado com o url-pattern “barra asterisco”. No Java EE tal filtro não é necessário, pois o controle de acesso é realizado pelo próprio container.
É importante notar que o nome do filtro é ‘springSecurityFilterChain’ e não deve ser alterado, pois o Spring Security já espera que o filtro esteja com este nome, por convenção.
No Java EE as configurações de autenticação e autorização são feitas no web.xml. No Spring Security são feitas em um application context padrão do Spring Framework. Dessa forma, precisamos do listener ContextLoaderListener declarado no web.xml para carregar o application context na inicialização da aplicação web. O atributo contextConfigLocation do listener indica a localização do application context, neste caso, na raiz do classpath com o nome spring-security-config.xml. O web.xml da Listagem 1 aplica essas configurações.
Application context é o nome dado aos XMLs de configuração do Spring Framework. Esses arquivos são genéricos o suficiente para configurar qualquer tipo de aplicação. Em casos específicos, como do Spring Security, namespaces são utilizados para reduzir a quantidade de XML necessária.
Listagem 1. web.xml – Configuração do Spring Security no web.xml
<web-app>
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-security-config.xml</param-value>
</context-param>
</web-app>
Controle de acesso e autenticação no spring-security-config.xml
Agora é necessário criarmos o arquivo spring-security-config.xml, conforme a Listagem 2. Este arquivo, carregado pelo listener do web.xml, utiliza o namespace do Spring Security (http://www.springframework.org/schema/security) para declarar regras de autenticação e autorização. Declaramos este namespace como default para o XML (sem prefixo) e o prefixo “beans” para o namespace normal do Spring Framework.
Se o leitor utiliza a IDE Eclipse, aconselhamos a instalação do plugin Spring IDE (veja seção de links) para facilidades na edição de application contexts. Outras IDEs como NetBeans e IntelliJ IDEA também possuem excelentes plugins de integração com o Spring, vale à pena conferir.
Listagem 2. spring-security-config.xml – Arquivo de configuração do Spring Security
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-2.0.2.xsd">
<http auto-config="true">
<intercept-url pattern="/usuarios/**" access="ROLE_USUARIO,ROLE_ADMIN" />
<intercept-url pattern="/admin/**" access="ROLE_ADMIN" />
</http>
<authentication-provider>
<user-service>
<user name="joao" password="123" authorities="ROLE_USUARIO" />
<user name="admin" password="123" authorities="ROLE_ADMIN" />
</user-service>
</authentication-provider>
</beans:beans>
O controle de acesso é definido pela tag <intercept-url> interna à tag <http>. No atributo pattern definimos uma expressão que atenderá as URLs acessadas e no atributo access definimos os roles de usuários que poderão acessar as URLs, separados por vírgulas. Definimos que todas as URLs com prefixo /usuarios serão acessadas por usuários normais e administradores e URLs com prefixo /admin apenas por administradores.
Por default a sintaxe das expressões utilizadas no atributo pattern é a mesma utilizada no Ant. Porém, se desejado, pode-se alterá-la para seguir a sintaxe de expressões regulares. Para tal, basta adicionar o atributo path-type="regex" na tag <http>. Dessa forma é possível criar expressões tão complexas quanto necessário para atender às URLs, de acordo com os requisitos do desenvolvedor.
As tags <intercept-url> serão interpretadas em ordem de definição e a primeira a atender será usada. Dessa forma, os patterns mais específicos devem vir primeiro. Por exemplo, a expressão ‘/usuarios/vip/**’ deve ser declarada acima da expressão ‘/usuarios/**’, caso contrário a expressão ‘vip’ nunca será avaliada, pois a expressão ‘/usuarios/**’ também atende a URLs do tipo ‘/usuarios/vip/**’.
O atributo auto-config=“true” da tag <http> configura automaticamente a aplicação para utilizar login baseado em formulário. O JSP do formulário nem mesmo precisa ser codificado, o Spring Security irá gerá-lo dinamicamente conforme a Figura 2. Para mais informações sobre o auto-config=“true” veja o quadro “Entendendo o auto-config”.
Entendendo o auto-config
O atributo auto-config da tag <http> na configuração do Spring Security ativa as opções mais usadas do framework, ajudando a diminuir a quantidade de XML necessário. Quando setamos seu valor para “true” o Spring Security considera o XML como a seguir:
<http>
<form-login />
<anonymous />
<http-basic />
<logout />
<remember-me />
</http>
Isso configura o framework para utilizar autenticação por formulário e http-basic, bem como tratamento de usuários anônimos e previamente autenticados (remember-me).
Cada uma das tags possui atributos. Para modificá-los é necessário apenas escrever a tag específica, e o auto-config=true substituirá esta parte da configuração. As tags omitidas continuam sendo consideradas. Por exemplo, o código abaixo muda o JSP do formulário de login e o restante das configurações automáticas continuam aplicadas.
<http auto-config='true'>
<form-login login-page='/login.jsp'/>
</http>
Desta forma, a última coisa que nos resta é definir os usuários possíveis e seus papéis. No primeiro exemplo, para facilitar, iremos utilizar a tag <user-service> que permite definir usuários, senhas e roles no próprio arquivo XML (também é possível referenciar um arquivo de propriedades). Normalmente, em uma aplicação real, não é viável definir os usuários em arquivo. Nos próximos exemplos iremos apresentar alternativas.
Figura 2. Login gerado automaticamente pelo Spring Security.
Últimos passos
O esqueleto do projeto, juntamente com os JARs necessários para rodar os exemplos, podem ser vistos da Figura 3. Para ajudar na depuração da aplicação, através de log, criamos o arquivo log4j.properties e adicionamos o log4j.jar ao classpath. O JAR do banco de dados HSQL-DB está presente pois será utilizado em breve, nos próximos exemplos. O resto das bibliotecas são dependências do Spring Security. Os JARs estão disponíveis no download dessa edição no site da Java Magazine.
Com os três JSPs, o web.xml, o spring-security-config.xml e os JARs necessários, podemos executar a aplicação. O deploy pode ser realizado em qualquer servidor da preferência do leitor, pois o Spring Security não requer configurações adicionais dependentes de container.
Executando o exemplo
Ao acessar o index da aplicação (Figura 1) dois links serão apresentados. Ao clicar em algum deles, por exemplo /usuarios/index.jsp, o filtro do Spring Security irá detectar como uma página protegida e irá gerar automaticamente o HTML para o login (Figura 2). Um usuário normal, ao logar-se, terá acesso à página de usuários, mas não à de administradores. Já o administrador tem acesso a ambas as páginas. Note que o serviço de “remeber-me” para o login já está funcionando. O exemplo é bastante simples, mas já cobre alguns dos principais conceitos e casos de uso.
Figura 3. Arquivos do primeiro exemplo.
Utilizando algoritmos de hash para senhas
No primeiro exemplo as senhas dos usuários estão visíveis aos administradores da aplicação. Isto apresenta um risco de segurança. Um algoritmo de hash é normalmente usado em uma aplicação real, impossibilitando o acesso à senha original. Para tal, o Spring Security oferece a tag <password-encoder>. São oferecidos os principais algoritmos de hash e também é possível criar uma implementação customizada. A Listagem 3 aplica o algoritmo “md5” como password-encoder.
Listagem 3. spring-security-config.xml – Utilizando um password-encoder
<beans:beans (...) >
(...)
<p align="left"> <authentication-provider>
<p align="left"> <password-encoder hash="md5" />
<p align="left">
<p align="left"> <user-service>
<p align="left"> <user name="joao" password="202cb962ac59075b964b07152d234b70" authorities="ROLE_USUARIO" />
<p align="left"> <user name="admin" password="202cb962ac59075b964b07152d234b70" authorities="ROLE_ADMIN" />
<p align="left"> </user-service>
<p align="left"> </authentication-provider>
(...)
</beans:beans>
Para gerar rapidamente um hash md5 para utilizar nos exemplos o leitor pode implementar uma classe Java simples de utilidade ou acessar um gerador online (veja Links).
Formulário personalizado
A geração automática de formulário é útil apenas para testes. Uma aplicação real necessita de uma página de formulário customizada. Iremos criar um JSP (/login.jsp) com um formulário para o nosso exemplo. Para isso acrescentamos este trecho dentro da tag <http> no spring-security-config.xml:
<form-login login-page="/login.jsp" authentication-failure-url="/login.jsp?login_error=true" />
O atributo authentication-failure-url configura o JSP que será apresentado caso o login falhe. Neste caso configuramos o mesmo JSP do formulário de login com um parâmetro login_error=true. Esse parâmetro é utilizado pelo JSP de login da Listagem 4.
Listagem 4. login.jsp – Página de login
<html>
<head>
<title>Spring Security</title>
</head>
<body>
<h1>Spring Security</h1><hr/>
<p>
<% if (request.getParameter("login_error") != null) { %>
<font color="red">
Não foi possível se autenticar.<br/>
Motivo: ${SPRING_SECURITY_LAST_EXCEPTION.message}.
</font>
<% } %>
</p>
<form action="j_spring_security_check" method="POST">
Login: <input type='text' name='j_username'
value="${not empty login_error ? SPRING_SECURITY_LAST_USERNAME : ''}" />
Senha: <input type='password' name='j_password'>
<input type="checkbox" name="_spring_security_remember_me" />
Salvar as minhas informações neste computador?
<input name="submit" type="submit" value=”Login” />
<input name="reset" type="reset" value=”Limpar” />
</form>
<a href="index.jsp">Voltar...</a><br>
</body>
</html>
Os exemplos demonstrados utilizam Scriplets (código Java no JSP) apenas por questões didáticas. Uma aplicação real estaria utilizando a taglib JSTL.
No topo do JSP, abaixo do título, testamos se o parâmetro login_error é igual a true. Caso verdade (o login falhou) então mostramos uma mensagem de erro. A expressão ${SPRING_SECURITY_LAST_EXCEPTION.message} recupera a última mensagem de erro gerada pelo framework. Essa mensagem por default está em inglês, mas pode ser internacionalizada.
Em seguida codificamos o formulário. Por default a action do formulário de login deve ser j_spring_security_check e o “name” dos inputs de usuário e senha j_username e j_password, respectivamente. No nosso exemplo, para habilitar o remember-me, adicionamos um checkbox com o “name” _spring_security_remember_me. Esse JSP gera a imagem da Figura 4.
Figura 4. Página de login customizada.
Logout
O atributo auto-config=true da tag <http> já configura uma URL default para realizar o logout, /j_spring_security_logout. Ao clicar em um link apontando para esta URL o logout é realizado. Caso necessário, é possível modificar a URL acrescentado a tag <logout> dentro da tag <http>:
<logout logout-url="/logout" />
Página de erro
Quando um usuário normal acessa a página protegida dos administradores o container apresenta uma mensagem padrão para o erro 403 (access denied). Essa página pode ser personalizada pelo Spring Security acrescentando o atributo access-denied-page à tag <http>:
<http access-denied-page="/accessDenied.jsp" (...) >
E então, criamos o accessDenied.jsp conforme a Listagem 5.
Listagem 5. accessDenied.jsp – Página de erro
<html>
<body>
<h1>Spring Security</h1><hr/>
<p><font color="red">Acesso negado. O usuário não tem permissão para acessar essa página.</font><p>
<p>Remote user....: <%= request.getRemoteUser() %></p>
<p>User principal....: <%= request.getUserPrincipal() %></p>
<a href="../">Voltar...</a><br>
<a href="../j_spring_security_logout">Logout</a><br>
</body>
</html>
Formulário de login embutido em outras páginas
Quem tem experiência com a segurança tradicional Java EE sabe que não é possível acessar o JSP de login diretamente pela aplicação. O container deve apresentar este JSP quando uma página protegida é acessada. Essa limitação cria outros problemas, por exemplo, quando um portal deseja que o formulário de login esteja contido em todas as áreas do site. Nesse caso, os desenvolvedores se obrigam a criar algum mecanismo utilizando JavaScript para contornar a situação.
No Spring Security tais limitações simplesmente não existem. Quando o usuário acessa diretamente o JSP de login, ou quando um formulário de login embutido é utilizado, o Spring Security utiliza uma URL default para redirecionar o usuário. A tag <form-login> possui um atributo default-target-url que indica esta URL (se não informada o default é a raiz da aplicação). Por exemplo, se o formulário for configurado desta forma:
<form-login default-target-url="/index.jsp" (...) />
Sempre que o formulário de login for acessado diretamente, após logar-se, o /index.jsp será carregado. Caso o usuário acessar uma página protegida, após logar-se, esta será a página exibida e não o /index.jsp. Se o atributo always-use-default-target for true o usuário sempre será redirecionado para o /index.jsp.
Autenticação utilizando banco de dados
A última modificação necessária para tornar o exemplo realístico é adicionar um banco de dados, substituindo a configuração dos usuários e roles do XML. Um modelo de dados típico para uma aplicação web, de maneira simplificada, é algo parecido com as tabelas da Figura 5.
Figura 5. Modelo de dados para armazenar os usuários, senhas e perfis.
Apesar de não ser necessário no nosso exemplo, criamos uma tabela de junção entre as tabelas de usuário e perfil para termos uma relação NxN. Este cenário é muito comum em aplicações enterprise.
O campo chamado ativo na tabela de usuários serve para impedir que usuários bloqueados autentiquem-se. Caso este campo for false o usuário está com o acesso bloqueado. O campo tentativas_login armazena o número de vezes que o usuário errou a senha consecutivamente. Esse campo será utilizado apenas em exemplos posteriores.
O download do artigo traz dois scripts para serem executados no banco de dados HSQL. Um script cria as tabelas e outro cria alguns usuários para teste. Para rodar o HSQL basta executar este comando no prompt do sistema operacional (considerando que o jar do HSQL está no diretório corrente):
java -cp hsqldb-1.8.0.7.jar org.hsqldb.Server
Logo após, abra outro prompt e execute o comando para abrir o console SQL com interface gráfica:
java -cp hsqldb-1.8.0.7.jar org.hsqldb.util.DatabaseManagerSwing
Selecione a opção “HSQL Database Engine Server” e pressione OK. Ao abrir o console execute os dois scripts para criar e popular as tabelas. Feche o console, mas mantenha sempre o prompt do servidor HSQL rodando.
Alterar a configuração do Spring Security para considerar as tabelas é algo muito simples, pressupondo que o banco de dados está corretamente configurado. Duas modificações são necessárias: substituir a tag <user-service> por uma tag <jdbc-user-service> e acrescentar um spring bean para a configuração do banco de dados (data-source). A Listagem 6 demonstra o novo spring-security-config.xml.
Listagem 6. spring-security-config.xml – Adicionando autenticação com banco de dados
<beans:beans (...)>
<http auto-config="true" access-denied-page="/accessDenied.jsp">
<intercept-url pattern="/usuarios/**" access="ROLE_USUARIO,ROLE_ADMIN" />
<intercept-url pattern="/admin/**" access="ROLE_ADMIN" />
<form-login login-page="/login.jsp"
authentication-failure-url="/login.jsp?login_error=true"
default-target-url="/index.jsp" />
</http>
<authentication-provider>
<password-encoder hash="md5" />
<jdbc-user-service data-source-ref="dataSource"
users-by-username-query="select login as username, senha as password, ativo as enabled from usuario where login = ?"
authorities-by-username-query="select u.login as username, p.descricao as authority from usuario u join usuario_perfil up on u.login = up.login join perfil p on up.id_perfil = p.id_perfil where u.login = ?" />
</authentication-provider>
<beans:bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<beans:property name="url" value="jdbc:hsqldb:hsql://localhost" />
<beans:property name="driverClassName" value="org.hsqldb.jdbcDriver" />
<beans:property name="username" value="sa" />
<beans:property name="password" value="" />
</beans:bean>
</beans:beans>
Precisamos de três atributos para configurar a tag <jdbc-user-service>. O primeiro atributo é uma referência para a configuração do banco de dados que será utilizado (data-source-ref). O segundo é a query que será utilizada para buscar os usuários dado um username (users-by-username-query). Essa query necessita retornar três colunas esperadas pelo Spring Security: username, password e enabled. Mapeamos as colunas do nosso modelo para as esperadas pela query. Por último, uma query para buscar os roles (authorities) do usuário, dado um username (authorities-by-username-query). Essa query retorna um username e a authority (role). Isso permite ao framework realizar as queries no banco e utilizar as informações retornadas para autenticação.
No final do arquivo de configuração adicionamos um bean “dataSource” e o configuramos para acesso ao banco de dados HSQL. Repare que utilizamos a classe DriverManagerDataSource que serve apenas para propósitos de teste. Em uma aplicação real configuraríamos um pool de conexões como data-source, normalmente oferecido pelo container através de um nome JNDI.
Eventos de autenticação e autorização
O Spring Security utiliza a infra-estrutura do application context do Spring para publicar eventos referentes a momentos importantes em seu fluxo de execução. Existem duas categorias de eventos: eventos de autenticação e eventos de autorização. Para cada situação existe um evento correspondente. Por exemplo, quando uma autenticação ocorre com sucesso, um evento AuthenticationSuccessEvent é publicado; quando a autenticação falha porque a senha está errada, um evento AuthenticationFailureBadCredentialsEvent é publicado; se a autenticação falha porque o usuário está inativo, um evento AuthenticationFailureDisabledEvent ocorre; e assim por diante. Quando uma URL é acessada e o filtro do Spring Security verifica se o usuário tem autorização para acessar a URL, eventos de sucesso ou falha também são publicados. Veja na Figura 6 uma hierarquia de eventos de autenticação e autorização. Consulte o Javadoc de cada classe para entender o momento em que cada evento é publicado.
Figura 6. Eventos de autenticação e autorização publicados pelo Spring Security.
Para “ouvirmos” aos eventos acima é necessário implementar a interface ApplicationListener e registrar a classe no application context. A Listagem 7 demonstra um exemplo simples de como ouvir a eventos de sucesso ou falha na autenticação.
Listagem 7. TestEventListener.java – Ouve eventos de autenticação publicados pelo Spring Security
public class TestEventListener implements ApplicationListener {
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof AuthenticationSuccessEvent) {
System.out.println("Usuário autenticado com sucesso");
}
if (event instanceof AbstractAuthenticationFailureEvent) {
System.out.println("Usuário não autenticado");
}
}
}
Lembre-se de adicionar o novo bean no application context:
<beans:bean class="br.com.jm.security.TestEventListener" />
Essa funcionalidade permite implementar complexos casos de uso, com baixo acoplamento entre a aplicação e o Spring Security, como veremos em um exemplo adiante. Para facilitar o aprendizado de quando os eventos ocorrem, registre dois listeners que vêm juntos com a distribuição do Spring Security, como a seguir:
<beans:bean class="org.springframework.security.event.authorization.LoggerListener" />
<beans:bean class="org.springframework.security.event.authentication.LoggerListener" />
Estes listeners efetuam log de todos os eventos que ocorrem, de autenticação e autorização. Uma ótima forma de aprender o funcionamento do framework é a análise do log gerado por estes listeners.
Explorando os pontos de extensão
Escolhemos um caso de uso para demonstrar como os eventos de autenticação são úteis e como é fácil customizar o framework. Cada vez que um usuário errar a senha por três vezes consecutivas, a sua conta será bloqueada. Essa funcionalidade é muito comum em sites que exigem altos padrões de segurança, como bancos por exemplo. A cada erro de autenticação por informar a senha incorreta (evento AuthenticationFailureBadCredentialsEvent) a coluna tentativas_login será incrementada. A cada autenticação com sucesso (evento AuthenticationSuccessEvent) a coluna tentativas_login será zerada. A cada tentativa de login, caso a coluna tentativas_login for igual ou maior que três, a autenticação será bloqueada. Veja na Listagem 8 o listener que atualiza a coluna tentativas_login.
Listagem 8. IncorrectPasswordEventListener.java – Listener que atualiza a coluna tentativas_login de acordo com o resultado da autenticação
public class IncorrectPasswordEventListener extends JdbcDaoSupport implements ApplicationListener {
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof AuthenticationFailureBadCredentialsEvent) {
AuthenticationFailureBadCredentialsEvent badCredentialsEvent = (AuthenticationFailureBadCredentialsEvent) event;
String sql = "update Usuario set tentativas_login = tentativas_login + 1 where login = ?";
this.executeSql(badCredentialsEvent, sql);
}
if (event instanceof AuthenticationSuccessEvent) {
AuthenticationSuccessEvent successEvent = (AuthenticationSuccessEvent) event;
String sql = "update Usuario set tentativas_login = 0 where login = ?";
this.executeSql(successEvent, sql);
}
}
private void executeSql(AbstractAuthenticationEvent event, String sql) {
getJdbcTemplate().update(sql, new Object[]{event.getAuthentication().getName()});
}
}
Para completar o exemplo ainda é necessário bloquear o login caso a coluna seja maior ou igual a três. Para isso iremos explorar um ponto de extensão comumente utilizado no Spring Security, a interface UserDetailsService. Essa interface possui o método loadUserByUsername() que recupera os dados de um usuário dado seu username. Veja a Listagem 9.
Listagem 9. Interface UserDetailsService e o método loadUserByUsername() a ser implementado
public class CustomUserDetailsService implements UserDetailsService {
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException, DataAccessException {
//retorna um objeto representado o usuário a ser autenticado
return null;
}
}
Uma implementação dessa interface substitui as tags <user-service> (usuários em XML ou arquivo de propriedades) ou <jdbc-user-service> (usuários em banco de dados) que vimos até agora. Sendo assim, através dessa implementação, o usuário do framework tem liberdade para buscar as informações de onde necessitar e da forma que quiser. Entretanto, o motivo mais comum para implementar a interface não é a escolha de uma nova fonte de dados e sim, a personalização do objeto UserDetails de retorno.
A interface UserDetails é implementada por objetos que representam o usuário no seu modelo de domínio. Ela possui métodos para retornar o username, o password, as authorities e quatro booleans representando diferentes motivos para bloqueio do login: enabled, accountNonLocked, accountNonExpired e credentialsNonExpired. Sendo assim, criamos uma classe Usuario para implementar a interface, veja a Listagem 10.
Listagem 10. Usuario.java – Objeto do modelo de domínio que implementa a interface UserDetails
public class Usuario implements UserDetails {
private String login;
private String senha;
private String email;
private boolean ativo;
private Integer tentativasLogin;
private GrantedAuthority[] authorities;
public String getUsername() {
return this.login;
}
public String getPassword() {
return this.senha;
}
public GrantedAuthority[] getAuthorities() {
return this.authorities;
}
public boolean isEnabled() {
return this.ativo;
}
public boolean isAccountNonLocked() {
return this.tentativasLogin < 3;
}
public boolean isAccountNonExpired() {
return true;
}
public boolean isCredentialsNonExpired() {
return true;
}
// --- restante dos getters and setters omitidos ---
}
Caso qualquer um dos métodos que retornam boolean retornar false o login será bloqueado e uma mensagem adequada será apresentada. Nesse caso não iremos utilizar as propriedades de conta ou senha expirada, então retornarmos sempre true nos métodos isAccountNonExpired() e isCredentialsNonExpired(). Para o método isEnable() utilizamos o valor da coluna ativo no banco de dados e para o método isAccountNonLocked() utilizamos nossa regra de negócios: bloquear o login caso três ou mais tentativas de login tenham sido feitas com a senha incorreta.
Para conectar as implementações precisamos de uma classe que implemente a interface UserDetailsService. Entretanto, queremos continuar utilizando um banco de dados para armazenar os usuários e apenas retornar um objeto Usuario com informações adicionais, as colunas email e tentativas_login. Para não duplicarmos código sem necessidade, iremos estender uma classe do Spring Security que já nos oferece um ponto de extensão justamente para estes casos. A classe JdbcDaoImpl é a classe por trás da tag <jdbc-user-service>. Essa classe já implementa o método loadUserByUsername() e nos deixa uma extensão valiosa, o método createUserDetails(). Esse método é um gancho para retornar um UserDetails customizado. Veja a Listagem 11.
Listagem 11. CustomUserDetailsService.java – Estende a classe JdbcDaoImpl para modificar o UserDetails de retorno
public class CustomUserDetailsService extends JdbcDaoImpl {
protected UserDetails createUserDetails(String username, UserDetails userFromUserQuery,
GrantedAuthority[] combinedAuthorities) {
Usuario usuario = new Usuario();
usuario.setLogin(userFromUserQuery.getUsername());
usuario.setSenha(userFromUserQuery.getPassword());
usuario.setAtivo(userFromUserQuery.isEnabled());
usuario.setAuthorities(combinedAuthorities);
this.carregarInformacoesAdicionais(usuario);
return usuario;
}
private void carregarInformacoesAdicionais(final Usuario usuario) {
String sql = "select email, tentativas_login from Usuario where login = ?";
getJdbcTemplate().query(sql, new Object[]{usuario.getUsername()}, new RowMapper() {
public Object mapRow(ResultSet rs, int rowNum) throws SQLException {
usuario.setEmail(rs.getString("email"));
usuario.setTentativasLogin(rs.getInt("tentativas_login"));
return null;
}
});
}
}
O método createUserDetails() apenas copia propriedades que já estão disponíveis e chama o método carregarInformacoesAdicionais() que faz uma query para preencher as propriedades restantes: email e tentativas_login.
A implementação do caso de uso está completa. A classe CustomUserDetailsService é utilizada para buscar as informações de um usuário e retornar um UserDetails do nosso domínio. Após a execução do método loadUserByUsername() as propriedades booleanas do Usuario são testadas. Nesse caso, o getter da propriedade accountNonLocked testa se a coluna tentativas_login é igual ou maior que três. Por fim, para controlar o incremento da coluna, os eventos de sucesso e falha na autenticação são tratados pela classe IncorrectPasswordEventListener. O novo spring-security-config.xml é demonstrado na Listagem 12.
Listagem 12. spring-security-config.xml – Versão final do XML de configuração refletindo o exemplo de bloqueio de login
<beans:beans (...) >
<http auto-config="true" access-denied-page="/accessDenied.jsp">
<intercept-url pattern="/usuarios/**" access="ROLE_USUARIO,ROLE_ADMIN" />
<intercept-url pattern="/admin/**" access="ROLE_ADMIN" />
<form-login login-page="/login.jsp" authentication-failure-url="/login.jsp?login_error=true" default-target-url="/index.jsp" />
</http>
<authentication-provider user-service-ref="customUserService">
<password-encoder hash="md5" />
</authentication-provider>
<beans:bean id="customUserService" class="br.com.jm.security.CustomUserDetailsService">
<beans:property name="dataSource" ref="dataSource" />
<beans:property name="usersByUsernameQuery" value="select login as username, senha as password, ativo as enabled from usuario where login = ?" />
<beans:property name="authoritiesByUsernameQuery" value="select u.login as username, p.descricao as authority from usuario u join usuario_perfil up on u.login = up.login join perfil p on up.id_perfil = p.id_perfil where u.login = ?" />
</beans:bean>
<beans:bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<beans:property name="url" value="jdbc:hsqldb:hsql://localhost" />
<beans:property name="driverClassName" value="org.hsqldb.jdbcDriver" />
<beans:property name="username" value="sa" />
<beans:property name="password" value="" />
</beans:bean>
<beans:bean class="br.com.jm.security.IncorrectPasswordEventListener">
<beans:property name="dataSource" ref="dataSource" />
</beans:bean>
<beans:bean class="org.springframework.security.event.authorization.LoggerListener" />
<beans:bean class="org.springframework.security.event.authentication.LoggerListener" />
</beans:beans>
A principal diferença do novo XML é a tag <authentication-provider>. Retiramos o <jdbc-user-service> e fizemos uma referência para o bean “customUserService”. Como a classe CustomUserServiceDetails estende JdbcDaoImpl as propriedades das queries continuam existindo. Sendo assim, copiamos as propriedades da antiga <jdbc-user-service> para o novo bean. Outra diferença importante é a adição do IncorrectPasswordEventListener no final do arquivo.
Integração com a API de servlets
O filtro do Spring Security substitui a implementação original da interface ServletRequest por um wrapper (extensão da classe ServletRequestWrapper). Este wrapper implementa os métodos relacionados a segurança na interface ServletRequest: getRemoteUser(), isUserInRole() e getUserPrincipal(). Dessa forma, é possível utilizar os métodos tradicionais do Java EE como de costume, sem diferenças. Por exemplo, veja o trecho de código a seguir:
String role = request.getRemoteUser();
System.out.println("Role: " + role);
System.out.println(request.isUserInRole("ROLE_USUARIO"));
System.out.println(request.isUserInRole("ROLE_ADMIN"));
Wrapper
Um objeto wrapper é uma implementação do padrão de projeto Decorator. O objetivo deste desing pattern é adicionar ou substituir o comportamento de classes existentes de forma dinâmica. Isto é feito criando uma classe base, normalmente com o sufixo Wrapper ou Decorator, que implementa a interface a ser decorada e também recebe como parâmetro no construtor um objeto da mesma interface. Em seguida, todos os métodos da classe são implementados delegando para o objeto recebido no construtor. Por fim, quando se deseja adicionar ou mudar o comportamento da interface, basta estender o Wrapper e sobre-escrever os métodos desejados.
Este código funciona perfeitamente com o Spring Security. Se o usuário “admin” (com perfil de administrador) estiver logado, então a seqüência a ser impressa no console será:
Role: admin
false
true
O método getUserPrincipal() retorna um objeto que implementa a interface org.springframework.security.Authentication. Através desse objeto é possível retornar o usuário logado, como a seguir:
public Usuario getUsuarioLogado(HttpServletRequest request) {
Authentication authentication = (Authentication) request.getUserPrincipal();
if (authentication == null) return null;
return (Usuario) authentication.getPrincipal();
}
Outra forma de se obter o usuário logado, porém sem necessitar do HttpServletRequest, é através da classe SecurityContextHolder. Essa classe mantém o objeto Authentication em uma variável thread-local. O método a seguir pode ser implementado em uma classe de utilidade e pode ser chamado em qualquer ponto da aplicação:
public static Usuario getUsuarioLogado() {
Authentication authentication = (Authentication) SecurityContextHolder.getContext().getAuthentication();
if (authentication instanceof Usuario) {
return (Usuario) authentication.getPrincipal();
}
return null;
}
Utilizando as taglibs
O Spring Security fornece duas tags úteis para codificação de JSPs: <authorize> e <authentication>. Ambas podem ser importadas com a declaração a seguir:
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
A tag <authorize> é utilizada para mostrar/esconder informações de acordo com as permissões (roles) do usuário. Veja o código a seguir:
<sec:authorize ifAllGranted="ROLE_ADMIN">
<p>Você é um administrador e também pode acessar esta <a href="../admin/index.jsp">página</a>.<p>
</sec:authorize>
A tag possui três atributos exclusivos: ifAllGranted, ifAnyGranted e ifNotGranted. Todos os atributos suportam um ou mais roles separados por vírgula. O atributo ifAllGranted informa que o usuário tem que possuir todos os roles declarados para retornar true. O atributo ifAnyGranted necessita de apenas um dos roles para retornar true e o atributo ifNotGranted será true apenas se o usuário não possuir nenhum dos roles.
A tag <authentication> permite acessar as propriedades do objeto Authentication do usuário logado no JSP. Por exemplo, o trecho de código a seguir, codificado no index.jsp, irá exibir as informações do usuário utilizando a tag <authentication>:
<sec:authorize ifAnyGranted="ROLE_ADMIN,ROLE_USUARIO">
<b>Informações do usuário logado:</b><br>
Login: <sec:authentication property="principal.login" /><br>
Senha: <sec:authentication property="principal.senha" /><br>
E-mail: <sec:authentication property="principal.email" /><br>
<a href="j_spring_security_logout">Logout</a><br>
</sec:authorize>
Conclusões
Com os exemplos demonstrados podemos ver alguns dos principais casos de uso de segurança em uma aplicação web. O Spring Security centraliza as configurações em apenas um arquivo que será empacotado juntamente com o war da aplicação, dispensando configurações externas do container. A aplicação não fica apenas mais enxuta, como também mais extensível, como vimos através da interface UserDetailsService e dos eventos de autenticação e autorização. Dessa forma é possível implementar casos complexos de gerenciamento de usuários, de forma desacoplada do domínio de negócios da aplicação.
Alguns assuntos não foram abordados no artigo por motivo de espaço, porém o leitor pode consultar a documentação no site do framework. Algumas funcionalidades oferecidas pelo Spring Security e não apresentadas são: segurança a nível de métodos (utilizando anotações e AOP), suporte a HTTPS, controle de sessões concorrentes e diferentes formas de autenticação, como certificados X509, LDAP, etc. Para mais informações sobre single sign on consulte o quadro “Single sign on com CAS e OpenID”.
Single sign on com CAS e OpenID
Autenticação single sign on significa logar-se em uma aplicação e permanecer logado ao acessar outra aplicação do mesmo grupo. Essa funcionalidade é comumente oferecida por containers para as aplicações as quais ele gerencia. O Spring Security oferece suporte a single sign on para aplicações intranet através do Central Authentication Service (CAS) e aplicações internet através do OpenID.
O CAS é uma aplicação web (arquivo WAR) que é responsável por apresentar as telas de login das aplicações do mesmo grupo. Basicamente, todas as aplicações do grupo irão redirecionar o browser para a aplicação central na hora da autenticação, e após o login, o CAS irá retornar a URL para a aplicação que solicitou o login. Dessa forma, as configurações de autenticação ficam no WAR do CAS e nos demais WARs o Spring Security é usado para realizar a integração. Uma vantagem do CAS é o fato de existirem clientes para diversas linguagens como Java, .Net, PHP, Perl, etc. permitindo integração entre aplicações heterogêneas.
O OpenID, por sua vez, elimina a necessidade de ter vários logins em diferentes sites na internet. Ao criar um login em um dos providers de OpenID (veja Links) é possível utilizar o username e senha para acessar todos os sites que o suportam, sem necessitar logar-se múltiplas vezes. Grandes empresas já suportam o padrão como Sun, Google, IBM, Microsoft, etc. O Spring Security facilita o processo de habilitar sua aplicação para suportar logins provenientes de providers OpenID.
Links
www.springsource.org
Portal
do Spring Framework e sub-projetos
static.springframework.org/spring-security/site/index.html
Acesso
direto à home page do Spring Security
springide.org/blog
Download
do Spring IDE (plugin para o Eclipse)
www.jasig.org/cas
Homepage do
projeto CAS
openid.net
Homepage do OpenID
www.myopenid.com
Provider de logins
OpenID
md5-hash-online.waraxe.us
Gerador online de
hash md5
Artigos relacionados
-
Artigo
-
Artigo
-
Artigo
-
Artigo
-
Artigo