Executando autenticação de usuário em Java EE / JSF usando j_security_check

156

Pergunto-me qual é a abordagem atual em relação à autenticação do usuário para um aplicativo da Web que utiliza o JSF 2.0 (e se algum componente existe) e os principais mecanismos do Java EE 6 (permissões de logon / verificação / logouts) com informações do usuário em um JPA entidade. O tutorial do Oracle Java EE é um pouco escasso nisso (trata apenas de servlets).

Isso ocorre sem o uso de uma estrutura totalmente diferente, como Spring-Security (acegi) ou Seam, mas tentando permanecer esperançosamente com a nova plataforma Java EE 6 (perfil da web), se possível.

ngeek
fonte

Respostas:

85

Depois de pesquisar na Web e tentar várias maneiras diferentes, eis o que eu sugeriria para autenticação do Java EE 6:

Configure o domínio de segurança:

No meu caso, eu tinha os usuários no banco de dados. Então, segui esta postagem no blog para criar um JDBC Realm que pudesse autenticar usuários com base no nome de usuário e nas senhas com hash MD5 na minha tabela de banco de dados:

http://blog.gamatam.com/2009/11/jdbc-realm-setup-with-glassfish-v3.html

Nota: a postagem fala sobre um usuário e uma tabela de grupo no banco de dados. Eu tinha uma classe User com um atributo enumer UserType mapeado por meio de anotações javax.persistence no banco de dados. Eu configurei o domínio com a mesma tabela para usuários e grupos, usando a coluna userType como a coluna do grupo e funcionou bem.

Use autenticação de formulário:

Ainda seguindo a postagem do blog acima, configure seu web.xml e sun-web.xml, mas em vez de usar a autenticação BASIC, use o FORM (na verdade, não importa qual você usa, mas acabei usando o FORM). Use o HTML padrão, não o JSF.

Em seguida, use a dica do BalusC acima para inicializar com preguiça as informações do usuário no banco de dados. Ele sugeriu fazê-lo em um bean gerenciado, obtendo o principal do contexto de rostos. Eu usei, em vez disso, um bean de sessão com estado para armazenar informações da sessão para cada usuário, então injetei o contexto da sessão:

 @Resource
 private SessionContext sessionContext;

Com o diretor, posso verificar o nome de usuário e, usando o EJB Entity Manager, obter as informações do usuário no banco de dados e armazenar no meu SessionInformationEJB.

Sair:

Também procurei a melhor maneira de sair. O melhor que eu encontrei está usando um Servlet:

 @WebServlet(name = "LogoutServlet", urlPatterns = {"/logout"})
 public class LogoutServlet extends HttpServlet {
  @Override
  protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
   HttpSession session = request.getSession(false);

   // Destroys the session for this user.
   if (session != null)
        session.invalidate();

   // Redirects back to the initial page.
   response.sendRedirect(request.getContextPath());
  }
 }

Embora minha resposta esteja muito atrasada, considerando a data da pergunta, espero que isso ajude outras pessoas que acabam aqui do Google, exatamente como eu.

Tchau,

Vítor Souza

Vítor E. Silva Souza
fonte
15
Um pequeno conselho: você está usando request.getSession (false) e chamando invalidate (). request.getSession (false) pode retornar nulo se não houver sessão. Melhor verificar se é nulo o primeiro;)
Arjan Tijms
@Vitor: Oi ... Gostaria de dizer algo sobre quando é bom mudar da segurança baseada em contêiner para alternativas como shiro ou outras? Veja perguntas mais focadas aqui: stackoverflow.com/questions/7782720/…
Rajat Gupta
Parece que o Glassfish JDBC Realm não suporta armazenamento de hashes de senhas salgadas. É realmente a melhor prática usá-lo nesse caso?
Lii
Desculpe, não posso ajudá-lo. Eu não sou um especialista em Glassfish. Talvez faça essa pergunta em um novo tópico para ver o que as pessoas dizem?
Vítor E. Silva Souza
1
Lii, você pode trabalhar com sal usando o recipiente glassfish. Configure seu healm para não usar nenhum hash. Ele compara o valor simples que você insere para a senha HttpServletResponse#login(user, password); dessa forma, você pode obter do DB o sal do usuário, as iterações e o que quer que seja usado para salgar, hash a senha que o usuário digitou usando esse sal e, em seguida, solicita que o contêiner se autentique com HttpServletResponse#login(user, password).
emportella
152

Suponho que você deseja autenticação baseada em formulário usando descritores de implantação e j_security_check.

Você também pode fazer isso no JSF usando apenas os mesmos nomes de campos predefinidos j_usernamee j_passwordconforme demonstrado no tutorial.

Por exemplo

<form action="j_security_check" method="post">
    <h:outputLabel for="j_username" value="Username" />
    <h:inputText id="j_username" />
    <br />
    <h:outputLabel for="j_password" value="Password" />
    <h:inputSecret id="j_password" />
    <br />
    <h:commandButton value="Login" />
</form>

Você pode fazer um carregamento lento no Usergetter para verificar se o Userjá está logado e, se não estiver, verifique se ele Principalestá presente na solicitação e, se estiver, obtenha o Userassociado j_username.

package com.stackoverflow.q2206911;

import java.io.IOException;
import java.security.Principal;

import javax.faces.bean.ManagedBean;
import javax.faces.bean.SessionScoped;
import javax.faces.context.FacesContext;

@ManagedBean
@SessionScoped
public class Auth {

    private User user; // The JPA entity.

    @EJB
    private UserService userService;

    public User getUser() {
        if (user == null) {
            Principal principal = FacesContext.getCurrentInstance().getExternalContext().getUserPrincipal();
            if (principal != null) {
                user = userService.find(principal.getName()); // Find User by j_username.
            }
        }
        return user;
    }

}

O Useré obviamente acessível no JSF EL por #{auth.user}.

Para sair, faça um HttpServletRequest#logout()(e defina Usercomo nulo!). Você pode obter um identificador do HttpServletRequestJSF por ExternalContext#getRequest(). Você também pode invalidar completamente a sessão.

public String logout() {
    FacesContext.getCurrentInstance().getExternalContext().invalidateSession();
    return "login?faces-redirect=true";
}

Para o restante (definindo usuários, funções e restrições no descritor e no domínio de implementação), basta seguir o tutorial do Java EE 6 e a documentação do servletcontainer da maneira usual.


Atualização : você também pode usar o novo Servlet 3.0 HttpServletRequest#login()para fazer um login programático, em vez de usar o j_security_checkque pode não ser acessível por um despachante em alguns contêineres de servlet. Neste caso, você pode usar um formulário JSF fullworthy e um feijão com usernamee passwordpropriedades e um loginmétodo que se parecem com isto:

<h:form>
    <h:outputLabel for="username" value="Username" />
    <h:inputText id="username" value="#{auth.username}" required="true" />
    <h:message for="username" />
    <br />
    <h:outputLabel for="password" value="Password" />
    <h:inputSecret id="password" value="#{auth.password}" required="true" />
    <h:message for="password" />
    <br />
    <h:commandButton value="Login" action="#{auth.login}" />
    <h:messages globalOnly="true" />
</h:form>

E essa visão no escopo do bean gerenciado que também se lembra da página solicitada inicialmente:

@ManagedBean
@ViewScoped
public class Auth {

    private String username;
    private String password;
    private String originalURL;

    @PostConstruct
    public void init() {
        ExternalContext externalContext = FacesContext.getCurrentInstance().getExternalContext();
        originalURL = (String) externalContext.getRequestMap().get(RequestDispatcher.FORWARD_REQUEST_URI);

        if (originalURL == null) {
            originalURL = externalContext.getRequestContextPath() + "/home.xhtml";
        } else {
            String originalQuery = (String) externalContext.getRequestMap().get(RequestDispatcher.FORWARD_QUERY_STRING);

            if (originalQuery != null) {
                originalURL += "?" + originalQuery;
            }
        }
    }

    @EJB
    private UserService userService;

    public void login() throws IOException {
        FacesContext context = FacesContext.getCurrentInstance();
        ExternalContext externalContext = context.getExternalContext();
        HttpServletRequest request = (HttpServletRequest) externalContext.getRequest();

        try {
            request.login(username, password);
            User user = userService.find(username, password);
            externalContext.getSessionMap().put("user", user);
            externalContext.redirect(originalURL);
        } catch (ServletException e) {
            // Handle unknown username/password in request.login().
            context.addMessage(null, new FacesMessage("Unknown login"));
        }
    }

    public void logout() throws IOException {
        ExternalContext externalContext = FacesContext.getCurrentInstance().getExternalContext();
        externalContext.invalidateSession();
        externalContext.redirect(externalContext.getRequestContextPath() + "/login.xhtml");
    }

    // Getters/setters for username and password.
}

Desta forma, o Useré acessível no JSF EL por #{user}.

BalusC
fonte
1
Atualizei a pergunta para incluir um aviso de isenção de responsabilidade de que a remessa j_security_checknão funcione em todos os servletcontainers.
BalusC
1
Link do Tutorial Java sobre o uso de segurança programática com aplicativos da Web: java.sun.com/javaee/6/docs/tutorial/doc/gjiie.html (usando Servlets): em uma classe de servlet, você pode usar: @WebServlet(name="testServlet", urlPatterns={"/ testServlet "}) @ServletSecurity(@HttpConstraint(rolesAllowed = {"testUser", "admin”})) E por método nível: @ServletSecurity(httpMethodConstraints={ @HttpMethodConstraint("GET"), @HttpMethodConstraint(value="POST", rolesAllowed={"testUser"})})
ngeek
3
E seu ponto é ..? Se isso é aplicável no JSF? Bem, no JSF há apenas um servlet, FacesServlete você não pode (e não deseja) modificá-lo.
precisa saber é o seguinte
1
@BalusC - Quando você diz que o acima é o melhor caminho, você quer usar o j_security_check ou um login programático?
18712 Simgineer
3
@simgineer: o URL solicitado está disponível como um atributo de solicitação com o nome definido por RequestDispatcher.FORWARD_REQUEST_URI. Os atributos de solicitação estão no JSF disponíveis por ExternalContext#getRequestMap().
BalusC
7

Deve-se mencionar que é uma opção deixar completamente os problemas de autenticação para o controlador frontal, por exemplo, um servidor Web Apache e avaliar o HttpServletRequest.getRemoteUser (), que é a representação JAVA da variável de ambiente REMOTE_USER. Isso também permite designs sofisticados de login, como autenticação Shibboleth. A filtragem de solicitações para um contêiner de servlet por meio de um servidor da Web é um bom design para ambientes de produção, geralmente o mod_jk é usado para isso.

Matthias Ronge
fonte
4

O problema HttpServletRequest.login não define o estado de autenticação na sessão foi corrigido no 3.0.1. Atualize o glassfish para a versão mais recente e pronto.

A atualização é bastante direta:

glassfishv3/bin/pkg set-authority -P dev.glassfish.org
glassfishv3/bin/pkg image-update
Hans Bacher
fonte
4
link está quebrado. a que questão você estava se referindo?
21812 Simgineer