在大多数网站上,当用户即将提供登录系统的用户名和密码时,会出现一个“保持登录状态”的复选框。如果选中该复选框,它将使您在同一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>
如果您使用的是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();
}
}
}
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);
}
}
你可以在a real world example找到Java EE Kickoff Application。
如果你使用的是Java EE 6或7,那么当用户没有登录但cookie存在时,可以生成一个长期存在的cookie来跟踪唯一客户端并使用Servlet 3.0 API提供的程序化登录HttpServletRequest#login()
。
如果您创建另一个数据库表,其中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()
上映射的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
应该是以秒为单位的年龄,例如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);
}
与那些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
值也是一种很好的做法。
请升级。
通常这样做是这样的:
当您登录用户时,您还在客户端上设置了一个cookie(并将cookie值存储在数据库中),在一段时间后(通常为1-2周)到期。
当有新请求进入时,请检查某个cookie是否存在,如果是,请查看数据库以查看它是否与某个帐户匹配。如果匹配,您将“松散地”登录该帐户。当我松散地说我的意思是你只让那个会话读取一些信息而不是写信息。您需要请求密码才能允许写入选项。
这就是全部。诀窍是确保“松散”登录不能对客户造成很大伤害。这有点保护用户免于抓住他记住我的cookie并试图以他身份登录的人。
您无法通过HttpServletRequest.login(用户名,密码)完全登录用户,因为您不应在数据库中同时保留用户名和纯文本密码。此外,您无法使用保存在数据库中的密码哈希执行此登录。但是,您需要使用基于Glassfish服务器API的自定义登录模块(Java类)来识别具有cookie / DB令牌的用户,而不使用自定义登录模块(Java类)输入密码。
有关详细信息,请参阅以下链接:
http://www.lucubratory.eu/custom-jaas-realm-for-glassfish-3/
虽然BalusC(Java EE 6/7的部分)的答案提供了有用的提示,但我不能在现代容器中工作,因为您无法将登录过滤器映射到以标准方式受保护的页面(如评论)。
如果由于某种原因你不能使用Spring Security(以不兼容的方式重新实现Servlet安全性),那么最好继续使用<auth-method>FORM
并将所有逻辑放入一个活动的登录页面。
这是代码(完整项目在这里:https://github.com/basinilya/rememberme)
veb.hml:
<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...
这是一些解释:
我们不是调用request.login()
而是建立与HTTP侦听器的新TCP连接,并将登录表单发布到/j_security_check
地址。这允许容器将我们重定向到最初请求的网页并恢复POST数据(如果有的话)。尝试从会话属性或RequestDispatcher.FORWARD_SERVLET_PATH
获取此信息将是特定于容器的。
我们不使用servlet过滤器进行自动登录,因为容器在到达过滤器之前转发/重定向到登录页面。
动态登录页面完成所有工作,包括:
/j_security_check
要实现“保持登录状态”功能,我们将提交的登录表单中的凭据保存在servlet上下文属性中(暂时)。与上面的SO答案不同,密码不会被散列,因为只有某些设置接受(具有jdbc领域的Glassfish)。持久cookie与凭证相关联。
流程如下:
<form-error-page>
然后呈现表单和错误消息/j_security_check
并重定向到结果(可能是我们再次)/j_security_check
/j_security_check
的代码使用当前的JSESSIONID
cookie以及来自真实表单或与持久性cookie相关联的凭证发送POST请求。