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>
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 .
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.
Veuillez mettre à niveau.
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.
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/
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:
/j_security_check
sous le capotPour 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:
<form-error-page>
, Rendez le formulaire et le message d'erreur/j_security_check
Et redirigez vers le résultat (qui pourrait être nous à nouveau)/j_security_check
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.