56

在大多数网站上,当用户准备提供用户名和密码登录系统时,会有一个复选框,如“保持登录状态”。如果您选中该框,它将让您从同一个 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>
4

4 回答 4

112

Java EE 8 及更高版本

如果您使用的是 Java EE 8 或更高版本,请@RememberMe使用自定义HttpAuthenticationMechanismRememberMeIdentityStore.

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

}

您可以在Java EE Kickoff Application中找到一个真实的示例


Java EE 6/7

如果您使用的是 Java EE 6 或 7,则自制一个长期存在的 cookie 来跟踪唯一客户端,并HttpServletRequest#login()在用户未登录但 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>

以及映射到doPost()的 a 方法中的以下内容: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);
    }
}

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

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

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

结合那些 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);
}

尽管UUID暴力破解非常困难,但您可以为用户提供一个选项,将“记住”选项锁定到用户的 IP 地址 ( request.getRemoteAddr()) 并将其存储/比较在数据库中。这使它更加健壮。此外,将“到期日期”存储在数据库中会很有用。

UUID每当用户更改其密码时,替换该值也是一个好习惯。


Java EE 5 或更低版本

请升级。

于 2011-02-22T20:50:19.167 回答
22

通常这样做是这样的:

当您登录用户时,您还会在客户端上设置一个 cookie(并将 cookie 值存储在数据库中),该 cookie 会在特定时间(通常为 1-2 周)后过期。

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

这就是全部。诀窍是确保“松散”登录不会对客户端造成很大伤害。这将在一定程度上保护用户免受某人抓取他的记住我的 cookie 并尝试以他的身份登录。

于 2011-02-22T20:30:00.350 回答
4

您不能通过 HttpServletRequest.login(username, password) 完全登录用户,因为您不应该在数据库中同时保留用户名和纯文本密码。此外,您不能使用保存在数据库中的密码哈希执行此登录。但是,您需要使用基于 Glassfish 服务器 API 的自定义登录模块(Java 类)来识别具有 cookie/DB 令牌的用户,但无需输入密码即可登录。

有关详细信息,请参阅以下链接:

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

Java EE 6/7 应用程序中的自定义安全机制

于 2014-11-28T20:34:23.197 回答
0

尽管 BalusC(Java EE 6/7 的部分)的答案提供了有用的提示,但我不适用于现代容器,因为您无法将登录过滤器映射到以标准方式保护的页面(如在评论)。

如果由于某种原因您不能使用 Spring Security(它以一种不兼容的方式重新实现了 Servlet Security),那么最好继续使用<auth-method>FORM并将所有逻辑放入一个活动的登录页面中。

这是代码(完整的项目在这里:https ://github.com/basinilya/rememberme )

网页.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>

登录.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...

这里有一些解释:

我们没有调用,而是与request.login()我们的 HTTP 侦听器建立一个新的 TCP 连接,并将登录表单发布到该/j_security_check地址。这允许容器将我们重定向到最初请求的网页并恢复 POST 数据(如果有)。试图从会话属性中获取此信息,或者RequestDispatcher.FORWARD_SERVLET_PATH将是特定于容器的。

我们不使用 servlet 过滤器进行自动登录,因为容器在到达过滤器之前转发/重定向到登录页面。

动态登录页面完成所有工作,包括:

  • 实际呈现登录表单
  • 接受填写的表格
  • /j_security_check在引擎盖下调用
  • 显示登录错误
  • 自动登录
  • 重定向回最初请求的页面

为了实现“保持登录”功能,我们将提交的登录表单中的凭据保存在 servlet 上下文属性中(现在)。与上面的 SO 答案不同,密码没有经过哈希处理,因为只有某些设置接受(带有 jdbc 领域的 Glassfish)。持久 cookie 与凭证相关联。

流程如下:

  • 转发/重定向到登录表单
  • 如果我们作为<form-error-page>then 呈现表单和错误消息
  • 否则,如果提交了一些凭据,则存储它们并调用/j_security_check并重定向到结果(可能又是我们)
  • 否则,如果找到 cookie,则检索关联的凭据并继续/j_security_check
  • 如果以上都不是,则呈现没有错误消息的登录表单

代码使用当前cookie 和来自真实表单或与持久 cookie 关联的凭据/j_security_check发送 POST 请求。JSESSIONID

于 2019-03-15T10:29:13.813 回答