Как реализовать «Оставайтесь в системе», когда пользователь входит в веб-приложение - PullRequest
53 голосов
/ 22 февраля 2011

На большинстве веб-сайтов, когда пользователь собирается ввести имя пользователя и пароль для входа в систему, имеется флажок «Оставаться в системе».Если вы установите флажок, он будет входить во все сеансы из одного и того же веб-браузера.Как я могу реализовать то же самое в 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 ]

105 голосов
/ 22 февраля 2011

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 Application.


Java EE 6/7

Если вы используете Java EE 6 или 7, создайте долговременный файл cookie для отслеживания уникального клиента и использования сервлета.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 или ниже

Пожалуйста, обновите.

22 голосов
/ 22 февраля 2011

Обычно это делается так:

Когда вы входите в систему, вы также устанавливаете cookie на клиенте (и сохраняете значение cookie в базе данных), срок действия которого истекает через определенное время (1-2 недели).обычно).

При поступлении нового запроса вы проверяете, существует ли определенный файл cookie, и, если это так, загляните в базу данных, чтобы определить, соответствует ли он определенному аккаунту.Если он совпадает, вы будете «свободно» входить в эту учетную запись.Когда я говорю свободно, я имею в виду, что вы позволяете этой сессии только читать некоторую информацию, а не записывать информацию.Вам нужно будет запросить пароль, чтобы разрешить опции записи.

Это все, что есть.Хитрость заключается в том, чтобы убедиться, что «слабо» вход в систему не может причинить много вреда клиенту.Это в некоторой степени защитит пользователя от того, кто захватит его cookie-файл "Помни меня" и попытается войти в систему под своим именем.

4 голосов
/ 28 ноября 2014

Вы не можете полностью войти в систему через HttpServletRequest.login (имя пользователя, пароль), поскольку вы не должны хранить в базе данных как имя пользователя, так и пароль в виде простого текста. Также вы не можете выполнить эту регистрацию с паролем, который сохранен в базе данных. Однако вам необходимо идентифицировать пользователя по токену cookie / DB, но войти в него / нее без ввода пароля, используя пользовательский модуль входа (класс Java) на основе API сервера Glassfish.

Для получения более подробной информации см. Следующие ссылки:

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

Пользовательский механизм безопасности в приложении Java EE 6/7

0 голосов
/ 15 марта 2019

Хотя ответ 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, используя текущий файл cookie JSESSIONID и учетные данные либо из реальной формы, либо связанные с постоянным файлом cookie.

...