Статьи

Apache Wicket: функциональность Запомнить меня

В веб-приложениях довольно часто используется функция «Запомнить меня», которая позволяет автоматически входить в систему при каждом посещении нашего веб-сайта.

Такая возможность может быть реализована с использованием Spring Security , но, на мой взгляд, использование инфраструктуры аутентификации на основе запросов с веб-инфраструктурой на основе компонентов – не лучшая идея. Эти два мира просто плохо сочетаются друг с другом, поэтому я предпочитаю использовать свое собственное запеченное решение, которое я представлю ниже.

Базовый проект

Мы начнем с простого веб-приложения, написанного с использованием новейшей, еще горячей версии Apache Wicket 6 . Вы можете скачать полные исходники с GitHub и запустить приложение, используя mvn clean compile jetty: run .

База приложения состоит из двух страниц:

  • Домашняя страница: отображает приветственное сообщение для зарегистрированных и незарегистрированных пользователей, либо для выхода из системы, либо для входа в систему.
  • Страница входа в систему: позволяет пользователю войти в систему на основе простой коллекции пользователей в памяти. Некоторые рабочие пары логин / пароль: Джон / Джон, Лиза / Лиза, Том / Том.

Запомнить меня функциональность

Стандартный способ реализации функции «Помни меня» выглядит следующим образом:

  1. Спросите пользователя, хочет ли он, чтобы его запомнили и автоматически зарегистрировали в будущем.
  2. Если это так, сохраните куки с логином и паролем на своем компьютере.
  3. Для каждого нового пользователя, заходящего на наш сайт, проверьте наличие файлов cookie с шага 2 и, если да, автоматический вход пользователя.
  4. Когда он вручную выходит из системы, удаляет куки, чтобы можно было очистить данные, используемые для автоматического входа.

Пункт 2 нуждается в пояснениях. В этом примере приложения мы собираемся сохранить логин, а не хешированный, не зашифрованный пароль в куки. В реальном сценарии это недопустимо. Вместо этого вам следует подумать о сохранении хешированного и засоленного пароля, чтобы, даже если кто-то перехватывает cookie-файл пользователя, пароль все равно будет секретным, и для его декодирования потребуется дополнительная работа.
Обновление: Миха? Mat? Oka опубликовал две очень интересные ссылки, как это можно сделать в реальных системах. Эти подходы даже не используют ни пароль, ни хеши паролей. Для более подробной информации, пожалуйста, смотрите в его комментарии ниже этого поста.

Шаг 1: Как пользователь, я хочу решить, хочу ли я использовать функцию «Запомнить меня»

Ссылка для фиксации с этим шагом

Чтобы позволить пользователю уведомить приложение о том, что он хочет использовать функцию «Запомнить меня», мы просто добавим флажок на страницу входа. Поэтому нам нужно немного изменить java и html-файл LoginPage (выделено новое):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<form wicket:id='form' class='form-horizontal'>
    <fieldset>
        <legend>Please login</legend>
    </fieldset>
 
    <div class='control-group'>
        <div wicket:id='feedback'></div>
    </div>
    <div class='control-group'>
        <label class='control-label' for='login'>Login</label>
        <div class='controls'>
            <input type='text' id='login' wicket:id='login' />
        </div>
    </div>
    <div class='control-group'>
        <label class='control-label' for='password'>Password</label>
        <div class='controls'>
            <input type='password' id='password' wicket:id='password' />
        </div>
    </div>
    <div class='control-group'>
        <div class='controls'>
            <label class='checkbox'>
                <input type='checkbox' wicket:id='rememberMe'> Remember me on this computer
            </label>
        </div>
    </div>
    <div class='form-actions'>
        <input type='submit' wicket:id='submit' value='Login' title='Login' class='btn btn-primary'/>
    </div>
</form>
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
private String login;
private String password;
private boolean rememberMe;
 
public LoginPage() {
 
    Form<Void> loginForm = new Form<Void>('form');
    add(loginForm);
 
    loginForm.add(new FeedbackPanel('feedback'));
    loginForm.add(new RequiredTextField<String>('login', new PropertyModel<String>(this, 'login')));
    loginForm.add(new PasswordTextField('password', new PropertyModel<String>(this, 'password')));
    loginForm.add(new CheckBox('rememberMe', new PropertyModel<Boolean>(this, 'rememberMe')));
 
    Button submit = new Button('submit') {
        // (...)
    };
 
    loginForm.add(submit);
}

Теперь мы готовы к следующему шагу.

Шаг 2: Как Систему я хочу сохранить логин и пароль в куки

Ссылка для фиксации с этим шагом

Во-первых, нам нужен CookieService, который инкапсулирует всю логику, отвечающую за работу с cookie-файлами: сохранение, распечатка и очистка cookie-файлов при необходимости. Код довольно прост, мы работаем с классами WebResponse и WebRequest, чтобы изменить куки в браузере пользователя.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class CookieService {
 
    public Cookie loadCookie(Request request, String cookieName) {
 
        List<Cookie> cookies = ((WebRequest) request).getCookies();
 
        if (cookies == null) {
            return null;
        }
 
        for (Cookie cookie : cookies) {
            if(cookie.getName().equals(cookieName)) {
                return cookie;
            }
        }
 
        return null;
    }
 
    public void saveCookie(Response response, String cookieName, String cookieValue, int expiryTimeInDays) {
        Cookie cookie = new Cookie(cookieName, cookieValue);
        cookie.setMaxAge((int) TimeUnit.DAYS.toSeconds(expiryTimeInDays));
        ((WebResponse)response).addCookie(cookie);
    }
 
    public void removeCookieIfPresent(Request request, Response response, String cookieName) {
        Cookie cookie = loadCookie(request, cookieName);
 
        if(cookie != null) {
            ((WebResponse)response).clearCookie(cookie);
        }
    }
}

Затем, когда пользователь проверяет «Запомнить меня» на LoginPage, мы должны сохранить куки в его браузере:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Button submit = new Button('submit') {
    @Override
    public void onSubmit() {
        UserService userService = WicketApplication.get().getUserService();
 
        User user = userService.findByLoginAndPassword(login, password);
 
        if(user == null) {
            error('Invalid login and/or password. Please try again.');
        }
        else {
            UserSession.get().setUser(user);
 
            if(rememberMe) {
                CookieService cookieService = WicketApplication.get().getCookieService();
                cookieService.saveCookie(getResponse(), REMEMBER_ME_LOGIN_COOKIE, user.getLogin(), REMEMBER_ME_DURATION_IN_DAYS);
                cookieService.saveCookie(getResponse(), REMEMBER_ME_PASSWORD_COOKIE, user.getPassword(), REMEMBER_ME_DURATION_IN_DAYS);
            }
 
            setResponsePage(HomePage.class);
        }
    }
};

Шаг 3: Как пользователь, я хочу автоматически регистрироваться, когда возвращаюсь в веб-приложение.

Ссылка для фиксации с этим шагом

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

Итак, давайте начнем с извлечения логики, связанной с сессией, в отдельный класс, называемый SessionProvider. Для проверки существующих пользователей и файлов cookie потребуется UserService и CookieService, поэтому мы передаем их в качестве ссылок в конструкторе.

01
02
03
04
05
06
07
08
09
10
11
public class WicketApplication extends WebApplication {
 
    private UserService userService = new UserService();
    private CookieService cookieService = new CookieService();
    private SessionProvider sessionProvider = new SessionProvider(userService, cookieService);
 
    @Override
    public Session newSession(Request request, Response response) {
        return sessionProvider.createNewSession(request);
    }
}

Роль SessionProvider заключается в создании новой UserSession, проверке наличия надлежащих файлов cookie и, если да, установке зарегистрированного пользователя. Кроме того, мы добавили сообщение обратной связи, чтобы сообщить пользователю, что он был автоматически зарегистрирован. Итак, давайте посмотрим на код:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class SessionProvider {
 
    public SessionProvider(UserService userService, CookieService cookieService) {
        this.userService = userService;
        this.cookieService = cookieService;
    }
 
    public WebSession createNewSession(Request request) {
        UserSession session = new UserSession(request);
 
        Cookie loginCookie = cookieService.loadCookie(request, REMEMBER_ME_LOGIN_COOKIE);
        Cookie passwordCookie = cookieService.loadCookie(request, REMEMBER_ME_PASSWORD_COOKIE);
 
        if(loginCookie != null && passwordCookie != null) {
            User user = userService.findByLoginAndPassword(loginCookie.getValue(), passwordCookie.getValue());
 
            if(user != null) {
                session.setUser(user);
                session.info('You were automatically logged in.');
            }
        }
 
        return session;
    }
}

Чтобы показать сообщение обратной связи на HomePage.java, мы должны добавить туда FeedbackPanel, но для краткости я опущу это в этом посте. Вы можете прочитать коммит, чтобы проверить, как это сделать.

Таким образом, после трех шагов у нас должно получиться «Помни меня». Чтобы проверить это быстро, измените время ожидания сессии в файле web.xml, добавив:

1
2
3
<session-config>
    <session-timeout>1</session-timeout>
</session-config>

а затем запустите приложение mvn clean compile jetty: запустите , перейдите на страницу входа, войдите, закройте браузер и через 1 минуту (после окончания сеанса) снова откройте его по адресу http: // localhost: 8080. Вы должны увидеть что-то вроде этого:

Так что это работает. Но нам все еще нужна еще одна вещь: разрешить пользователю удалять куки-файлы и отключать автоматический вход в систему.

Шаг 4: Как пользователь, я хочу иметь возможность выйти и очистить мои куки

Ссылка для фиксации с этим шагом
На последнем шаге мы должны позволить пользователю очистить свои данные и отключить «Запомнить меня» для своей учетной записи. Это будет достигнуто путем очистки обоих файлов cookie, когда пользователь явно нажимает на ссылку «Выйти».

01
02
03
04
05
06
07
08
09
10
11
12
13
Link<Void> logoutLink = new Link<Void>('logout') {
    @Override
    public void onClick() {
        CookieService cookieService = WicketApplication.get().getCookieService();
        cookieService.removeCookieIfPresent(getRequest(), getResponse(), SessionProvider.REMEMBER_ME_LOGIN_COOKIE);
        cookieService.removeCookieIfPresent(getRequest(), getResponse(), SessionProvider.REMEMBER_ME_PASSWORD_COOKIE);
 
        UserSession.get().setUser(null);
        UserSession.get().invalidate();
    }
};
logoutLink.setVisible(UserSession.get().userLoggedIn());
add(logoutLink);


Резюме

Вот и все. В этом порту мы реализовали простую функциональность «Запомнить меня» в веб-приложении, написанном с использованием Apache Wicket, без использования каких-либо внешних библиотек аутентификации.

Приятного кодирования и не забудьте поделиться!

Ссылка: функция «Помни меня» в Apache Wicket от нашего партнера по JCG Томаша Дзюрко из блога Code Hard Go Pro .