Olá Pessoal!
Depois de muito tempo sem postar (diríamos meses...), resolvi escrever sobre um tema que a algum tempo muitas pessoas vinham me pedindo sobre isso, e como eu consegui fazer uma solução para este "problema" resolvi escrever aqui, (não sei se é a forma certa, mas é funcional)!
O tema de hoje é sobre como ter um campo a mais no form de login utilizando o Spring Security, imagino eu que tenha uma forma mais simples de fazer... mas enquanto não descubro, vamos àquela que funciona.
Antes de começar o nosso projeto, é de total importância que você já conheça ou já tenha utilizado o Spring Security, caso não conheça, leia esta postagem.
Antes de começar o nosso projeto, é de total importância que você já conheça ou já tenha utilizado o Spring Security, caso não conheça, leia esta postagem.
* Começando *
Inicialmente a minha estrutura do banco de dados (básico):
Neste caso, eu tenho duas tabelas: uma de empresa onde a coluna regra será para dizer se é ROLE_ADM, ROLE_USER... entre outras, e tenho o usuário que está ligado diretamente a empresa, fiz o mais básico possível apenas para mostrar como que pode estar colocando um campo a mais no login, neste caso: o usuário terá que colocar seu login, senha e escolher a qual empresa pertence.
Neste caso, eu tenho duas tabelas: uma de empresa onde a coluna regra será para dizer se é ROLE_ADM, ROLE_USER... entre outras, e tenho o usuário que está ligado diretamente a empresa, fiz o mais básico possível apenas para mostrar como que pode estar colocando um campo a mais no login, neste caso: o usuário terá que colocar seu login, senha e escolher a qual empresa pertence.
Para entender direito, esta será a nossa tela de login:
O projeto completo terá esta estrutura:
Não dê tanta atenção quanto ao pacotes que se encontram as classes, pois esta apenas foi uma organização minha, acredito que cada um tenha uma forma melhor de organizar.
A primeira coisa a se fazer é baixar a biblioteca do Spring Security na versão 3.1.4 (pode baixar clicando aqui), juntamente com o Spring Framework 3.1.4. As bibliotecas que eu uso são:
O projeto completo terá esta estrutura:
Não dê tanta atenção quanto ao pacotes que se encontram as classes, pois esta apenas foi uma organização minha, acredito que cada um tenha uma forma melhor de organizar.
A primeira coisa a se fazer é baixar a biblioteca do Spring Security na versão 3.1.4 (pode baixar clicando aqui), juntamente com o Spring Framework 3.1.4. As bibliotecas que eu uso são:
Inicialmente é necessário fazer as classes de modelo, que estão no pacote javasemcafe.model, observo que fiz este projeto utilizando JPA, mas pra quem quiser usar apenas JDBC é só fazer as adaptações, utilizei o JPA por ser mais rápido, então de acordo com a estrutura de banco de dados teremos duas classes, uma é Empresa e a outra é Usuário:
Classe Empresa.java:
@Entity
@Table(name = "empresa")
public class Empresa implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@Basic(optional = false)
@NotNull
@Column(name = "codigo")
private Integer codigo;
@Size(max = 45)
@Column(name = "descricao")
private String descricao;
@Size(max = 45)
@Column(name = "regra")
private String regra;
// Getters e Setters
}
Classe Usuario.java:
@Entity
@Table(name = "usuario")
public class Usuario implements Serializable {
@Id
@Basic(optional = false)
@NotNull
@Column(name = "codigo")
private Integer codigo;
@Size(max = 45)
@Column(name = "login")
private String login;
@Size(max = 45)
@Column(name = "senha")
private String senha;
@JoinColumn(name = "empresa_codigo", referencedColumnName = "codigo")
@ManyToOne(optional = false, fetch = FetchType.LAZY)
private Empresa empresa;
// Getters e Setters
}
No pacote javasemcafe.dao, temos a classe EmpresaDAO.java:
@Stateless
public class EmpresaDAO {
@PersistenceContext(unitName = "LoginCustomizadoPU")
private EntityManager em;
public List<Empresa> listar() {
List<Empresa> empresas = null;
try {
Query query = em.createQuery("Select e from Empresa e order by e.descricao");
empresas = query.getResultList();
} catch (Exception e) {
e.printStackTrace();
}
return empresas;
}
}
Nesta classe, apenas temos um método que retorna uma lista de empresas que será utilizada com o EmpresaBean.java para popular o combo de empresas na página de login.
Classe EmpresaBean.java:
@ManagedBean
@ViewScoped
public class EmpresaBean implements Serializable {
@EJB
private EmpresaDAO empresaDAO;
private List<Empresa> empresas;
public List<Empresa> getEmpresas() {
empresas = empresaDAO.listar();
return empresas;
}
public void setEmpresas(List<Empresa> empresas) {
this.empresas = empresas;
}
}
Neste caso apenas preenche uma Collection que aqui é o List.
Voltando ao pacote javasemcafe.dao, temos praticamente a classe mais importante:
AutenticacaoFilter.java, é nela que fazemos a verificação de login, senha e se o usuário pertence ou não a empresa selecionada no login, abaixo explico os detalhes dela:
public class AutenticacaoFilter extends UsernamePasswordAuthenticationFilter {
private EntityManagerFactory factory = Persistence.createEntityManagerFactory("LoginCustomizadoPU");
private EntityManager em;
private String mensagem;
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, BadCredentialsException {
String login = request.getParameter("j_login");
String senha = request.getParameter("j_senha");
Integer codigoEmpresa = Integer.parseInt(request.getParameter("j_empresa"));
try {
Usuario usuario = buscarUsuario(login, senha);
if (usuario != null) {
if (usuario.getEmpresa().getCodigo().equals(codigoEmpresa)) {
Collection<GrantedAuthority> regras = new ArrayList<GrantedAuthority>();
regras.add(new SimpleGrantedAuthority(usuario.getEmpresa().getRegra()));
request.getSession().setAttribute("usuarioLogado", usuario);
mensagem = "Bem vindo: " + usuario.getLogin();
return new UsernamePasswordAuthenticationToken(usuario.getLogin(), usuario.getSenha(), regras);
} else {
mensagem = "Acesso negado a empresa selecionada!";
throw new BadCredentialsException(mensagem);
}
} else {
mensagem = "Dados Incorretos";
throw new BadCredentialsException(mensagem);
}
} catch (Exception e) {
throw new BadCredentialsException(e.getMessage());
}
}
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, Authentication authResult) throws IOException, ServletException {
SecurityContextHolder.getContext().setAuthentication(authResult);
request.getSession().setAttribute("msg", mensagem);
response.sendRedirect("index.jsf");
}
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
request.getSession().setAttribute("msg", mensagem);
response.sendRedirect("login.jsf");
}
/**
* Metodos de acesso ao BD
*
*/
public Usuario buscarUsuario(String login, String senha) {
em = factory.createEntityManager();
Usuario usuario = null;
try {
Query query = em.createQuery("SELECT u FROM Usuario u WHERE u.login = :login AND u.senha = :senha");
query.setParameter("login", login);
query.setParameter("senha", senha);
usuario = (Usuario) query.getSingleResult();
} catch (NoResultException e) {
System.out.println("DAO: Não foi encontrado resultado!");
} catch (Exception e) {
e.printStackTrace();
} finally {
em.close();
}
return usuario;
}
}
Vamos entender o que acontece nessa classe: os métodos attemptAuthentication. successfulAuthentication e unsuccessfulAuthentication são métodos criados automaticamente quando se utiliza o extends UsernamePasswordAuthenticationFilter. O primeiro método é onde ocorre a verificação do usuário, é o comando executado quando o usuário clica no botão de "Acessar" (conforme tela no início da postagem), nesse método tem um detalhe importante, que é a linha 23 onde ele dá o return com os dados do usuário, no caso das regras são aqueles ROLE_ADM, ROLE_USER.... isso é necessário para o applicationContext.xml saber se deve liberar a pagina ou não, veremos mais pra frente isso, o que você precisa saber é que, dependendo a lógica que você utilizar para montar a estrutura do BD, será possível que um usuário pertença a diferentes filiais com diferentes regras para cada uma, mas aí teria que fazer uma tabela N-N, o que prologaria ainda mais essa postagem que já vai ficar enorme. Só mais um detalhe antes de continuar, na linha 21, nós jogamos o usuário para a sessão da aplicação, assim podemos recuperar ele a qualquer momento. Continuando, se no primeiro método ocorrer o return ele vai para o método de sucesso (successfulAuthentication), onde eu digo para o Spring Security que há uma autenticação e redireciono o usuário para a página inicial. No caso de não ocorrer um return, por dados incorretos, pelo usuário não ter acesso a empresa, automaticamente cairá no método unsuccessfulAuthentication, onde eu redireciono ele para a página de login novamente.
Vamos a uma observação nessa classe: percebam que eu tenho uma String mensagem, pois bem, ela não seria necessária se caso não quisesse mostrar ao usuário os avisos de dados incorretos e outros... então eu coloco essa String na sessão da aplicação, e recupero por uma outra classe que é a AutenticacaoPhaseListener.java, que se encontra no pacote javasemcafe.controller.
public class AutenticacaoPhaseListener implements PhaseListener {
@Override
public void afterPhase(PhaseEvent event) {
//não implementado
}
@Override
public void beforePhase(PhaseEvent event) {
FacesContext context = FacesContext.getCurrentInstance();
HttpSession session = (HttpSession) context.getExternalContext().getSession(false);
if (session != null) {
String mensagem = (String) session.getAttribute("msg");
if (mensagem != null) {
context.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, mensagem, null));
session.setAttribute("msg", null);
}
}
}
@Override
public PhaseId getPhaseId() {
return PhaseId.RESTORE_VIEW;
}
}
Esta classe implementa um PhaseListener que vai pegar a mensagem na sessão da aplicação e vai mandá-la por por um FacesMessage para o JSF.
Utilizando a classe UsuarioBean.java, do pacote javasemcafe.controller(esta classe não está aparecendo na imagem da estrutura do projeto, mas deve estar no projeto), vamos pegar o usuário na sessão:
@ManagedBean
@ViewScoped
public class UsuarioBean {
private Usuario usuarioLogado;
public Usuario getUsuario() {
FacesContext context = FacesContext.getCurrentInstance();
HttpSession session = (HttpSession) context.getExternalContext().getSession(false);
usuarioLogado = (Usuario) session.getAttribute("usuarioLogado");
return usuarioLogado;
}
public void setUsuario(Usuario usuario) {
this.usuarioLogado = usuario;
}
}
Veja que é utilizado o atributo "usuarioLogado" que foi setado lá na classe AutenticacaoFilter.java, é através dela que conseguimos pegar esse usuário. Ou você pode estar pegando o usuário diretamente do Spring Security, porém neste caso ele não te trás muitas informações de usuário, mas sei que o login é uma delas, veja um exemplo (isto não está no projeto, é apenas uma outra forma de um outro projeto meu):
public ConfiguracaoBean() {
Authentication authentication = (Authentication) SecurityContextHolder.getContext().getAuthentication();
if (authentication != null) {
usuario = authentication.getName();
}
}
Agora vamos para as páginas do JSF, começando pela index.xhtml (também conhecida: index.jsf) que será a página para onde o usuário será redirecionado se logar o sistema:
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html">
<h:head>
<title>Facelet Title</title>
</h:head>
<h:body>
<h:outputText value="Olá #{usuarioBean.usuario.login}!"/><br/>
<h:outputText value="Você está na empresa: #{usuarioBean.usuario.empresa.descricao}!"/><br/>
<a href="./logout">Sair</a>
</h:body>
</html>
Não tem nada de mais, só estou usando a classe UsuarioBean.java para mostrar o usuário que está logado na sessão da aplicação. Ficando desta forma:
Página de login, login.xhtml:
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core">
<h:head>
<title>Login Customizado</title>
</h:head>
<h:body>
<form action="j_spring_security_check" method="post" style="text-align: right;">
<fieldset>
<legend>Login</legend>
<h:panelGrid columns="2">
<h:outputText value="Login:" />
<h:inputText id="j_login" />
<h:outputText value="Senha:" />
<h:inputSecret id="j_senha" />
<h:outputText value="Empresa:" />
<h:selectOneMenu id="j_empresa" style="width: 155px" >
<f:selectItems value="#{empresaBean.empresas}" var="e" itemLabel="#{e.descricao}" itemValue="#{e.codigo}" />
</h:selectOneMenu>
<h:outputText />
<h:commandButton value="Acessar" id="btnAcessar" />
</h:panelGrid>
<h:message for="btnAcessar" />
</fieldset>
</form>
</h:body>
</html>
A única coisa diferente do normal das páginas de login do Spring Security, é o caso de ter adicionado o h:selectOneMenu para fazer o combo, fora isso, o restante já foi explicado na postagem que eu indico para leitura no começo do post. No caso da h:message serve para mostrar as mensagens enviadas pelo FacesMessage, aparecendo dessa forma:
Vamos ver agora os arquivos de configuração que ficam dentro do WEB-INF.
Começando com a configuração básica no web.xml para o Spring Security ser identificado (já explicado na postagem indicada):
Começando com a configuração básica no web.xml para o Spring Security ser identificado (já explicado na postagem indicada):
<!-- Spring security -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<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>
<!-- Fim spring security -->
Bom, como devem saber na versão JEE 6, o faces-config.xml é opcional, ou seja é utilizado em alguns casos, como o caso agora, onde eu preciso "registrar" o PhaseListener (a classe AutenticacaoPhaseListener.java):
<faces-config version="2.1"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_2_1.xsd">
<lifecycle>
<phase-listener>javasemcafe.controller.AutenticacaoPhaseListener</phase-listener>
</lifecycle>
</faces-config>
Agora vamos para toda a "mágica" da customização do login, o applicationContext.xml:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:security="http://www.springframework.org/schema/security"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.1.xsd">
<security:http auto-config="false" use-expressions="true" access-denied-page="/index.jsf" entry-point-ref="authenticationEntryPoint" >
<security:intercept-url pattern="/index.jsf" access="hasRole('ROLE_ADM')"/>
<security:logout invalidate-session="true" logout-success-url="/login.jsf" logout-url="/logout"/>
<security:custom-filter ref="authenticationFilter" position="FORM_LOGIN_FILTER"/>
</security:http>
<bean id="authenticationFilter" class="javasemcafe.dao.AutenticacaoFilter" p:authenticationManager-ref="authenticationManager"/>
<bean id="authenticationEntryPoint" class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint" p:loginFormUrl="/login.jsf"/>
<security:authentication-manager alias="authenticationManager" />
</beans>
O que vemos de diferente na configuração do applicationContext, é a partir da 15º linha, onde de uma forma geral, estou dizendo que a autenticação deverá ser feita pela classe: javasemcafe.dao.AutenticacaoFilter, como pode ver na linha 18.
Bom pessoal, acabei deixando de explicar muitas classes (do Spring Security) porque senão essa postagem iria ficar bem mais longa!
Até a próxima :)
andii.brunetta





