web-dev-qa-db-fra.com

Comment implémenter "Rester connecté" lorsque l'utilisateur se connecte à l'application Web

Sur la plupart des sites Web, lorsque l'utilisateur est sur le point de fournir le nom d'utilisateur et le mot de passe pour se connecter au système, il y a une case à cocher comme "Rester connecté". Si vous cochez la case, cela vous gardera connecté à toutes les sessions à partir du même navigateur Web. Comment puis-je implémenter la même chose dans Java EE?

J'utilise l'authentification gérée par conteneur basée sur FORM avec une page de connexion JSF.

<security-constraint>
    <display-name>Student</display-name>
    <web-resource-collection>
        <web-resource-name>CentralFeed</web-resource-name>
        <description/>
        <url-pattern>/CentralFeed.jsf</url-pattern>
    </web-resource-collection>        
    <auth-constraint>
        <description/>
        <role-name>STUDENT</role-name>
        <role-name>ADMINISTRATOR</role-name>
    </auth-constraint>
</security-constraint>
 <login-config>
    <auth-method>FORM</auth-method>
    <realm-name>jdbc-realm-scholar</realm-name>
    <form-login-config>
        <form-login-page>/index.jsf</form-login-page>
        <form-error-page>/LoginError.jsf</form-error-page>
    </form-login-config>
</login-config>
<security-role>
    <description>Admin who has ultimate power over everything</description>
    <role-name>ADMINISTRATOR</role-name>
</security-role>    
<security-role>
    <description>Participants of the social networking Bridgeye.com</description>
    <role-name>STUDENT</role-name>
</security-role>
53
Thang Pham

Java EE 8 et plus

Si vous êtes sur Java EE 8 ou plus récent, mettez @RememberMe sur un --- HttpAuthenticationMechanism personnalisé avec un RememberMeIdentityStore .

@ApplicationScoped
@AutoApplySession
@RememberMe
public class CustomAuthenticationMechanism implements HttpAuthenticationMechanism {

    @Inject
    private IdentityStore identityStore;

    @Override
    public AuthenticationStatus validateRequest(HttpServletRequest request, HttpServletResponse response, HttpMessageContext context) {
        Credential credential = context.getAuthParameters().getCredential();

        if (credential != null) {
            return context.notifyContainerAboutLogin(identityStore.validate(credential));
        }
        else {
            return context.doNothing();
        }
    }
}
public class CustomIdentityStore implements RememberMeIdentityStore {

    @Inject
    private UserService userService; // This is your own EJB.

    @Inject
    private LoginTokenService loginTokenService; // This is your own EJB.

    @Override
    public CredentialValidationResult validate(RememberMeCredential credential) {
        Optional<User> user = userService.findByLoginToken(credential.getToken());
        if (user.isPresent()) {
            return new CredentialValidationResult(new CallerPrincipal(user.getEmail()));
        }
        else {
            return CredentialValidationResult.INVALID_RESULT;
        }
    }

    @Override
    public String generateLoginToken(CallerPrincipal callerPrincipal, Set<String> groups) {
        return loginTokenService.generateLoginToken(callerPrincipal.getName());
    }

    @Override
    public void removeLoginToken(String token) {
        loginTokenService.removeLoginToken(token);
    }

}

Vous pouvez trouver n exemple réel dans Java EE Kickoff Application .


Java EE 6/7

Si vous êtes sur Java EE 6 ou 7, homegrow un cookie de longue durée pour suivre le client unique et utiliser la connexion programmatique fournie par l'API Servlet 3.0 HttpServletRequest#login() lorsque l'utilisateur n'est pas connecté mais que le cookie est présent.

C'est le plus simple à réaliser si vous créez une autre table DB avec une valeur Java.util.UUID Comme PK et l'ID de l'utilisateur en question comme FK.

Supposons le formulaire de connexion suivant:

<form action="login" method="post">
    <input type="text" name="username" />
    <input type="password" name="password" />
    <input type="checkbox" name="remember" value="true" />
    <input type="submit" />
</form>

Et ce qui suit dans la méthode doPost() d'un Servlet qui est mappé sur /login:

String username = request.getParameter("username");
String password = hash(request.getParameter("password"));
boolean remember = "true".equals(request.getParameter("remember"));
User user = userService.find(username, password);

if (user != null) {
    request.login(user.getUsername(), user.getPassword()); // Password should already be the hashed variant.
    request.getSession().setAttribute("user", user);

    if (remember) {
        String uuid = UUID.randomUUID().toString();
        rememberMeService.save(uuid, user);
        addCookie(response, COOKIE_NAME, uuid, COOKIE_AGE);
    } else {
        rememberMeService.delete(user);
        removeCookie(response, COOKIE_NAME);
    }
}

(le COOKIE_NAME doit être le nom unique du cookie, par exemple "remember" et le COOKIE_AGE doit être l'âge en secondes, par exemple 2592000 Pendant 30 jours)

Voici à quoi pourrait ressembler la méthode doFilter() d'un Filter qui est mappé sur des pages restreintes:

HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
User user = request.getSession().getAttribute("user");

if (user == null) {
    String uuid = getCookieValue(request, COOKIE_NAME);

    if (uuid != null) {
        user = rememberMeService.find(uuid);

        if (user != null) {
            request.login(user.getUsername(), user.getPassword());
            request.getSession().setAttribute("user", user); // Login.
            addCookie(response, COOKIE_NAME, uuid, COOKIE_AGE); // Extends age.
        } else {
            removeCookie(response, COOKIE_NAME);
        }
    }
}

if (user == null) {
    response.sendRedirect("login");
} else {
    chain.doFilter(req, res);
}

En combinaison avec ces méthodes d'aide aux cookies (dommage qu'elles manquent dans l'API Servlet):

public static String getCookieValue(HttpServletRequest request, String name) {
    Cookie[] cookies = request.getCookies();
    if (cookies != null) {
        for (Cookie cookie : cookies) {
            if (name.equals(cookie.getName())) {
                return cookie.getValue();
            }
        }
    }
    return null;
}

public static void addCookie(HttpServletResponse response, String name, String value, int maxAge) {
    Cookie cookie = new Cookie(name, value);
    cookie.setPath("/");
    cookie.setMaxAge(maxAge);
    response.addCookie(cookie);
}

public static void removeCookie(HttpServletResponse response, String name) {
    addCookie(response, name, null, 0);
}

Bien que le UUID soit extrêmement difficile à utiliser par force brute, vous pouvez fournir à l'utilisateur une option pour verrouiller l'option "Remember" sur l'adresse IP de l'utilisateur (request.getRemoteAddr()) et la stocker/comparer dans le base de données ainsi. Cela le rend un peu plus robuste. De plus, avoir une "date d'expiration" stockée dans la base de données serait utile.

Il est également recommandé de remplacer la valeur UUID chaque fois que l'utilisateur a changé son mot de passe.


Java EE 5 ou inférieur

Veuillez mettre à niveau.

105
BalusC

Normalement, cela se fait comme ceci:

Lorsque vous vous connectez à un utilisateur, vous définissez également un cookie sur le client (et stockez la valeur du cookie dans la base de données) expirant après un certain temps (1-2 semaines généralement).

Lorsqu'une nouvelle demande arrive, vous vérifiez que le cookie existe et si c'est le cas, regardez dans la base de données pour voir s'il correspond à un certain compte. Si cela correspond, vous vous connecterez alors "de manière lâche" à ce compte. Quand je dis vaguement, je veux dire que vous ne laissez que cette session lire des informations et non écrire des informations. Vous devrez demander le mot de passe afin d'autoriser les options d'écriture.

C'est tout ce qui est. L'astuce consiste à s'assurer qu'une connexion "vaguement" n'est pas en mesure de faire beaucoup de mal au client. Cela protégera quelque peu l'utilisateur de quelqu'un qui attrape son cookie Remember me et essaie de se connecter en tant que lui.

22
Mihai Toader

Vous ne pouvez pas vous connecter complètement à un utilisateur via HttpServletRequest.login (nom d'utilisateur, mot de passe) car vous ne devez pas conserver à la fois le nom d'utilisateur et le mot de passe en texte brut dans la base de données. De plus, vous ne pouvez pas effectuer cette connexion avec un hachage de mot de passe qui est enregistré dans la base de données. Cependant, vous devez identifier un utilisateur avec un cookie/jeton de base de données mais le connecter sans entrer de mot de passe à l'aide du module de connexion personnalisé (classe Java) basé sur l'API du serveur Glassfish.

Voir les liens suivants pour plus de détails:

http://www.lucubratory.eu/custom-jaas-realm-for-glassfish-3/

Mécanisme de sécurité personnalisé dans Java EE 6/7

4
John Mikic

Bien que la réponse de BalusC (la partie pour Java EE 6/7) donne des conseils utiles, je ne travaille pas dans des conteneurs modernes, car vous ne pouvez pas mapper un filtre de connexion sur des pages qui sont protégés de manière standard (comme confirmé dans les commentaires).

Si, pour une raison quelconque, vous ne pouvez pas utiliser Spring Security (qui réimplémente la sécurité des servlets de manière incompatible), il est préférable de rester avec <auth-method>FORM Et de mettre toute la logique dans une page de connexion active.

Voici le code (le projet complet est ici: https://github.com/basinilya/rememberme )

web.xml:

    <form-login-config>
        <form-login-page>/login.jsp</form-login-page>
        <form-error-page>/login.jsp?error=1</form-error-page>
    </form-login-config>

login.jsp:

if ("1".equals(request.getParameter("error"))) {
    request.setAttribute("login_error", true);
} else {
    // The initial render of the login page
    String uuid;
    String username;

    // Form fields have priority over the persistent cookie

    username = request.getParameter("j_username");
    if (!isBlank(username)) {
        String password = request.getParameter("j_password");

        // set the cookie even though login may fail
        // Will delete it later
        if ("on".equals(request.getParameter("remember_me"))) {
            uuid = UUID.randomUUID().toString();
            addCookie(response, COOKIE_NAME, uuid, COOKIE_AGE); // Extends age.
            Map.Entry<String,String> creds =
                    new AbstractMap.SimpleEntry<String,String>(username,password);
            rememberMeServiceSave(request, uuid, creds);
        }
        if (jSecurityCheck(request, response, username, password)) {
            return;
        }
        request.setAttribute("login_error", true);
    }

    uuid = getCookieValue(request, COOKIE_NAME);
    if (uuid != null) {
        Map.Entry<String,String> creds = rememberMeServiceFind(request, uuid);
        if (creds != null) {
            username = creds.getKey();
            String password = creds.getValue();
            if (jSecurityCheck(request, response, username, password)) {
                return; // going to redirect here again if login error
            }
            request.setAttribute("login_error", true);
        }
    }
}

// login failed
removeCookie(response, COOKIE_NAME);
// continue rendering the login page...

Voici quelques explications:

Au lieu d'appeler request.login(), nous établissons une nouvelle connexion TCP à notre écouteur HTTP et publions le formulaire de connexion à l'adresse /j_security_check. Cela permet au conteneur de rediriger nous à la page Web initialement demandée et restaurer les données POST (le cas échéant). Essayer d'obtenir ces informations à partir d'un attribut de session ou RequestDispatcher.FORWARD_SERVLET_PATH serait spécifique au conteneur.

Nous n'utilisons pas de filtre de servlet pour la connexion automatique, car les conteneurs transfèrent/redirigent vers la page de connexion AVANT que le filtre ne soit atteint.

La page de connexion dynamique fait tout le travail, y compris:

  • rendre réellement le formulaire de connexion
  • accepter le formulaire rempli
  • appelant /j_security_check sous le capot
  • affichage des erreurs de connexion
  • connexion automatique
  • rediriger vers la page initialement demandée

Pour implémenter la fonctionnalité "Rester connecté", nous enregistrons les informations d'identification du formulaire de connexion soumis dans l'attribut de contexte de servlet (pour l'instant). Contrairement à la réponse SO ci-dessus, le mot de passe n'est pas haché, car seules certaines configurations l'acceptent (Glassfish avec un domaine jdbc). Le cookie persistant est associé aux informations d'identification.

Le flux est le suivant:

  • Être transféré/redirigé vers le formulaire de connexion
  • Si nous sommes servis en tant que <form-error-page>, Rendez le formulaire et le message d'erreur
  • Sinon, si certaines informations d'identification sont soumises, stockez-les et appelez /j_security_check Et redirigez vers le résultat (qui pourrait être nous à nouveau)
  • Sinon, si le cookie est trouvé, récupérez les informations d'identification associées et continuez avec /j_security_check
  • Si rien de ce qui précède, restituez le formulaire de connexion sans le message d'erreur

Le code de /j_security_check Envoie une demande POST en utilisant le cookie JSESSIONID actuel et les informations d'identification à partir du formulaire réel ou associées au cookie persistant.

0
basin