Exécution de l'authentification utilisateur dans Java EE / JSF à l'aide de j_security_check

156

Je me demande quelle est l'approche actuelle concernant l'authentification des utilisateurs pour une application Web utilisant JSF 2.0 (et si des composants existent) et les mécanismes de base de Java EE 6 (connexion / vérification des autorisations / déconnexion) avec les informations utilisateur conservées dans un JPA entité. Le didacticiel Oracle Java EE est un peu rare à ce sujet (ne gère que les servlets).

C'est sans utiliser un tout autre framework, comme Spring-Security (acegi) ou Seam, mais en essayant de s'en tenir si possible à la nouvelle plate-forme Java EE 6 (profil Web).

ngeek
la source

Réponses:

85

Après avoir cherché sur le Web et essayé de nombreuses façons différentes, voici ce que je suggérerais pour l'authentification Java EE 6:

Configurez le domaine de la sécurité:

Dans mon cas, j'avais les utilisateurs dans la base de données. J'ai donc suivi cet article de blog pour créer un royaume JDBC qui pourrait authentifier les utilisateurs en fonction du nom d'utilisateur et des mots de passe hachés MD5 dans ma table de base de données:

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

Remarque: le message parle d'un utilisateur et d'une table de groupe dans la base de données. J'avais une classe User avec un attribut enum UserType mappé via des annotations javax.persistence à la base de données. J'ai configuré le royaume avec la même table pour les utilisateurs et les groupes, en utilisant la colonne userType comme colonne de groupe et cela a bien fonctionné.

Utilisez l'authentification par formulaire:

Toujours en suivant le billet de blog ci-dessus, configurez votre web.xml et sun-web.xml, mais au lieu d'utiliser l'authentification BASIC, utilisez FORM (en fait, peu importe celui que vous utilisez, mais j'ai fini par utiliser FORM). Utilisez le HTML standard, pas le JSF.

Ensuite, utilisez le conseil de BalusC ci-dessus sur l'initialisation paresseuse des informations utilisateur à partir de la base de données. Il a suggéré de le faire dans un bean géré en obtenant le principal du contexte des visages. J'ai utilisé, à la place, un bean session avec état pour stocker les informations de session pour chaque utilisateur, j'ai donc injecté le contexte de session:

 @Resource
 private SessionContext sessionContext;

Avec le mandant, je peux vérifier le nom d'utilisateur et, en utilisant le gestionnaire d'entités EJB, obtenir les informations utilisateur de la base de données et les stocker dans mon SessionInformationEJB.

Se déconnecter:

J'ai également cherché le meilleur moyen de me déconnecter. Le meilleur que j'ai trouvé utilise un 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());
  }
 }

Bien que ma réponse soit vraiment tardive compte tenu de la date de la question, j'espère que cela aidera d'autres personnes qui se retrouvent ici de Google, tout comme moi.

Ciao,

Vítor Souza

Vítor E. Silva Souza
la source
15
Un petit conseil: vous utilisez request.getSession (false) et appelez invalidate () à ce sujet. request.getSession (false) peut retourner null s'il n'y a pas de session. Mieux vaut vérifier d'abord si c'est nul;)
Arjan Tijms
@Vitor: Salut..Voulez-vous dire quelque chose sur le moment où il est bon de passer de la sécurité basée sur les conteneurs à des alternatives comme shiro ou autres? Voir la question plus ciblée ici: stackoverflow.com/questions/7782720/…
Rajat Gupta
Il semble que le Glassfish JDBC Realm ne prend pas en charge le stockage des hachages de mots de passe salés. Est-il vraiment recommandé de l'utiliser dans ce cas?
Lii
Désolé, je ne peux pas vous aider. Je ne suis pas un expert de Glassfish. Peut-être posez-vous cette question dans un nouveau fil pour voir ce que les gens disent?
Vítor E. Silva Souza
1
Lii, vous pouvez travailler avec du sel en utilisant le récipient en verre. Configurez votre santé pour ne pas utiliser de hachage. Il comparera la valeur simple que vous insérez pour le mot de passe HttpServletResponse#login(user, password), de cette façon, vous pouvez simplement obtenir de la base de données le sel de l'utilisateur, les itérations et tout ce que vous utilisez pour le salage, hacher le mot de passe que l'utilisateur a entré à l'aide de ce sel, puis demander au conteneur de s'authentifier avec HttpServletResponse#login(user, password).
emportella
152

Je suppose que vous voulez une authentification par formulaire utilisant des descripteurs de déploiement et j_security_check.

Vous pouvez également le faire dans JSF en utilisant simplement les mêmes noms de champ prédéfinis j_usernameet j_passwordcomme illustré dans le didacticiel.

Par exemple

<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>

Vous pouvez effectuer un chargement paresseux dans le Usergetter pour vérifier si le Userest déjà connecté et sinon, vérifier si le Principalest présent dans la requête et si c'est le cas, obtenir le Userassocié avec 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;
    }

}

Le Userest évidemment accessible dans JSF EL par #{auth.user}.

Pour vous déconnecter, faites un HttpServletRequest#logout()(et définissez-le Usersur null!). Vous pouvez obtenir une poignée de HttpServletRequestJSF dans ExternalContext#getRequest(). Vous pouvez également simplement invalider complètement la session.

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

Pour le reste (définition des utilisateurs, des rôles et des contraintes dans le descripteur de déploiement et le domaine), suivez simplement le didacticiel Java EE 6 et la documentation servletcontainer de la manière habituelle.


Mise à jour : vous pouvez également utiliser le nouveau Servlet 3.0 HttpServletRequest#login()pour effectuer une connexion par programme au lieu d'utiliser j_security_checkce qui peut ne pas être accessible en soi par un répartiteur dans certains conteneurs de servlet. Dans ce cas , vous pouvez utiliser une forme fullworthy JSF et un haricot avec usernameet passwordpropriétés et une loginméthode qui ressemblent à ceci:

<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>

Et cette vue a une portée gérée bean qui se souvient également de la page initialement demandée:

@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.
}

De cette façon, le Userest accessible dans JSF EL par #{user}.

BalusC
la source
1
J'ai mis à jour la question pour inclure une clause de non-responsabilité selon laquelle l'envoi j_security_checkpeut ne pas fonctionner sur tous les conteneurs de servlet.
BalusC
1
Lien du tutoriel Java sur l'utilisation de la sécurité programmatique avec les applications Web: java.sun.com/javaee/6/docs/tutorial/doc/gjiie.html (en utilisant des servlets): Sur une classe de servlet, vous pouvez utiliser: @WebServlet(name="testServlet", urlPatterns={"/ testServlet "}) @ServletSecurity(@HttpConstraint(rolesAllowed = {"testUser", "admin”})) Et sur une méthode par méthode niveau: @ServletSecurity(httpMethodConstraints={ @HttpMethodConstraint("GET"), @HttpMethodConstraint(value="POST", rolesAllowed={"testUser"})})
ngeek
3
Et votre point étant ..? Est-ce que cela est applicable dans JSF? Eh bien, dans JSF, il n'y a qu'un seul servlet, FacesServletet vous ne pouvez pas (et ne voulez pas) le modifier.
BalusC
1
@BalusC - Quand vous dites que ce qui précède est le meilleur moyen, voulez-vous dire utiliser j_security_check ou une connexion par programmation?
simgineer
3
@simgineer: l'URL demandée est disponible en tant qu'attribut de requête avec le nom défini par RequestDispatcher.FORWARD_REQUEST_URI. Les attributs de demande sont dans JSF disponibles par ExternalContext#getRequestMap().
BalusC
7

Il faut mentionner que c'est une option pour laisser complètement les problèmes d'authentification au contrôleur frontal, par exemple un serveur Web Apache et évaluer à la place HttpServletRequest.getRemoteUser (), qui est la représentation JAVA de la variable d'environnement REMOTE_USER. Cela permet également des conceptions de connexion sophistiquées telles que l'authentification Shibboleth. Le filtrage des demandes vers un conteneur de servlet via un serveur Web est une bonne conception pour les environnements de production, souvent mod_jk est utilisé pour le faire.

Matthias Ronge
la source
4

Le problème HttpServletRequest.login ne définit pas l'état d'authentification dans la session a été résolu dans la version 3.0.1. Mettez à jour glassfish vers la dernière version et vous avez terminé.

La mise à jour est assez simple:

glassfishv3/bin/pkg set-authority -P dev.glassfish.org
glassfishv3/bin/pkg image-update
Hans Bacher
la source
4
le lien est rompu. de quel problème faisiez-vous allusion?
simgineer