当用户登录Web应用程序时,如何实现"保持登录状态"

Tha*_*ham 53 forms-authentication java-ee stay-logged-in

在大多数网站上,当用户即将提供登录系统的用户名和密码时,会出现一个"保持登录状态"的复选框.如果选中该复选框,它将使您在同一Web浏览器的所有会话中登录.如何在Java EE中实现相同的功能?

我正在使用基于FORM的容器管理身份验证和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>
Run Code Online (Sandbox Code Playgroud)

Bal*_*usC 107

Java EE 8及更高版本

如果你在Java EE的8或更新版本,把@RememberMe对定制HttpAuthenticationMechanism了一起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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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);
    }

}
Run Code Online (Sandbox Code Playgroud)

您可以在Java EE Kickoff应用程序中找到一个真实的示例.


Java EE 6/7

如果你使用的是Java EE 6或7,那么HttpServletRequest#login()当用户没有登录但cookie存在时,可以生成一个长期存在的cookie来跟踪唯一的客户端并使用Servlet 3.0 API提供的程序化登录.

如果您创建另一个java.util.UUID值为PK的数据库表以及相关用户的ID为FK,则这是最容易实现的.

假设以下登录表单:

<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>
Run Code Online (Sandbox Code Playgroud)

并且在以下doPost()的方法Servlet,其上映射/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);
    }
}
Run Code Online (Sandbox Code Playgroud)

(COOKIE_NAME应该是唯一的cookie名称,例如"remember",COOKIE_AGE应该是以秒2592000为单位的年龄,例如30天)

以下是在受限页面上映射的doFilter()方法Filter如下所示:

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);
}
Run Code Online (Sandbox Code Playgroud)

与那些cookie帮助器方法结合使用(在Servlet API中缺少它们太糟糕了):

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);
}
Run Code Online (Sandbox Code Playgroud)

虽然UUID非常难以暴力破解,但您可以为用户提供一个选项,将"记住"选项锁定到用户的IP地址(request.getRemoteAddr())并在数据库中存储/比较它.这使得它更加强大.此外,在数据库中存储"到期日期"将是有用的.

UUID每当用户更改密码时替换值也是一种很好的做法.


Java EE 5或更低版本

请升级.

  • @Marcio:通过各种方式,无论是意外还是意识,cookie都可能被盗,暴露或被篡改.您永远不应该在cookie中存储敏感的用户特定信息,如用户名和密码.UUID只是确保了一个独特且不可评估的价值,这本身就是无用的信息. (3认同)
  • 如果你将@BalusC的答案与很多问题结合起来,肯定会成为你书架上必备的书. (3认同)
  • 这只是因为最终用户有任何以前的cookie,但后来决定禁用"记住我".不,"uuid"在这里不合适,因为表中不存在.只需删除与用户ID关联的所有条目. (2认同)
  • 我很遗憾重新开始这个旧讨论,但我有一个问题@BalusC:你是否在用户对象中以明文形式存储密码?(看起来你必须这样,因为你调用login(... user.getPassword());这是如何在你的持久性中表示的? (2认同)

Mih*_*der 22

通常这样做是这样的:

当您登录用户时,您还在客户端上设置了一个cookie(并将cookie值存储在数据库中),在一段时间后(通常为1-2周)到期.

当有新请求进入时,请检查某个cookie是否存在,如果是,请查看数据库以查看它是否与某个帐户匹配.如果匹配,您将"松散地"登录该帐户.当我松散地说我的意思是你只让那个会话读取一些信息而不是写信息.您需要请求密码才能允许写入选项.

这就是全部.诀窍是确保"松散"登录不能对客户造成很大伤害.这有点保护用户免于抓住他记住我的cookie并试图以他身份登录的人.