Как реализовать "Оставайтесь в системе", когда пользователь входит в веб-приложение
На большинстве веб-сайтов, когда пользователь собирается ввести имя пользователя и пароль для входа в систему, имеется флажок "Оставаться в системе". Если вы установите этот флажок, он будет входить во все сеансы из одного и того же веб-браузера. Как я могу реализовать то же самое в 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>
3 ответа
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();
}
}
}
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.
Java EE 6/7
Если вы используете Java EE 6 или 7, создайте долговременный файл cookie для отслеживания уникального клиента и используйте программный вход Servlet 3.0 API.HttpServletRequest#login()
когда пользователь не вошел в систему, но cookie присутствует.
Этого проще всего достичь, если создать другую таблицу БД сjava.util.UUID
значение в качестве PK и идентификатор рассматриваемого пользователя как 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
значение всякий раз, когда пользователь изменил свой пароль.
Java EE 5 или ниже
Пожалуйста, обновите.
Обычно это делается так:
Когда вы входите в систему, вы также устанавливаете cookie на клиенте (и сохраняете значение cookie в базе данных), срок действия которого истекает через определенное время (обычно 1-2 недели).
При поступлении нового запроса вы проверяете, существует ли определенный файл cookie, и, если это так, загляните в базу данных, чтобы убедиться, что он соответствует определенной учетной записи. Если он совпадает, вы будете "свободно" входить в эту учетную запись. Когда я говорю свободно, я имею в виду, что вы позволяете этой сессии только читать некоторую информацию, а не записывать информацию. Вам нужно будет запросить пароль, чтобы разрешить опции записи.
Это все, что есть. Хитрость заключается в том, чтобы убедиться, что "слабо" вход в систему не может причинить много вреда клиенту. Это в некоторой степени защитит пользователя от кого-то, кто захватит его cookie-файл "Помни меня" и попытается войти в него как он.
Вы не можете полностью войти в систему через HttpServletRequest.login(имя пользователя, пароль), поскольку вы не должны хранить в базе данных как имя пользователя, так и пароль в виде простого текста. Также вы не можете выполнить этот вход с паролем хеш, который сохраняется в базе данных. Однако вам необходимо идентифицировать пользователя с помощью маркера cookie/DB, но войти в него / нее без ввода пароля, используя пользовательский модуль входа (класс Java) на основе API сервера Glassfish.
Смотрите следующие ссылки для более подробной информации:
http://www.lucubratory.eu/custom-jaas-realm-for-glassfish-3/
Пользовательский механизм безопасности в приложении Java EE 6/7
Хотя ответ BalusC (часть для Java EE 6/7) дает полезные советы, я не работаю с современными контейнерами, потому что вы не можете сопоставить фильтр входа в систему со страницами, которые защищены стандартным способом (как подтверждено в комментарии).
Если по какой-то причине вы не можете использовать Spring Security (который повторно реализует Servlet Security несовместимым способом), тогда лучше остаться с <auth-method>FORM
и поместите всю логику в активную страницу входа.
Вот код (полный проект здесь: 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...
Вот некоторые объяснения:
Вместо звонка request.login()
мы устанавливаем новое TCP-соединение с нашим слушателем HTTP и публикуем форму входа в /j_security_check
адрес. Это позволяет контейнеру перенаправить нас на первоначально запрошенную веб-страницу и восстановить данные POST (если есть). Попытка получить эту информацию из атрибута сеанса или RequestDispatcher.FORWARD_SERVLET_PATH
будет зависеть от контейнера.
Мы не используем фильтр сервлетов для автоматического входа в систему, потому что контейнеры пересылают / перенаправляют на страницу входа в систему ДО того, как фильтр будет достигнут.
Страница динамического входа делает всю работу, включая:
- фактически рендеринг формы входа
- принимая заполненную форму
- призвание
/j_security_check
под капотом - отображение ошибок входа
- автоматический вход
- перенаправление обратно на изначально запрашиваемую страницу
Для реализации функции "Оставайтесь в системе" мы сохраняем учетные данные из отправленной формы входа в атрибут контекста сервлета (на данный момент). В отличие от ответа SO выше, пароль не хэшируется, потому что это допускают только определенные установки (Glassfish с областью jdbc). Постоянный файл cookie связан с учетными данными.
Поток следующий:
- Получить перенаправлены / перенаправлены в форму входа
- Если мы служим
<form-error-page>
затем визуализируйте форму и сообщение об ошибке - В противном случае, если представлены некоторые учетные данные, сохраните их и позвоните
/j_security_check
и перенаправить на результат (который может быть снова нас) - В противном случае, если файл cookie найден, получите соответствующие учетные данные и продолжайте с
/j_security_check
- Если ничего из вышеперечисленного, то визуализировать форму входа без сообщения об ошибке
Код для /j_security_check
отправляет запрос POST, используя текущий JSESSIONID
cookie и учетные данные либо из реальной формы, либо связанные с постоянным cookie.