Перейти к основному содержанию

Авторизация и управление сессиями

В этом примере мы создадим простое приложение авторизации с login/sign up сценариями и управлением сессиями.

ActiveJ не включает встроенные модули или решения для авторизации, поскольку этот процесс может существенно отличаться в зависимости от бизнес-логики проекта. Этот пример представляет собой простую "лучшую практику", которую вы можете расширять и изменять в зависимости от ваших потребностей. Полные исходные тексты примеров вы можете найти на GitHub

В примере мы рассмотрим только сервер , который был создан с помощью ActiveJ HttpServerLauncher и AsyncServlet. Такой подход позволяет создать встроенный сервер приложений примерно в 100 строках кода без дополнительных XML-конфигураций или зависимостей от сторонних производителей.

Создание программы запуска

Создадим AuthLauncher, который является основной частью приложения, поскольку управляет жизненным циклом приложения, маршрутизацией и процессами авторизации. Мы будем использовать ActiveJ HttpServerLauncher и расширим его:

public final class AuthLauncher extends HttpServerLauncher {  public static final String SESSION_ID = "SESSION_ID";
  @Provides  AuthService loginService() {    return new AuthServiceImpl();  }
  @Provides  Executor executor() {    return newSingleThreadExecutor();  }
  @Provides  StaticLoader staticLoader(Executor executor) {    return StaticLoader.ofClassPath(executor, "site/");  }
  @Provides  SessionStore<String> sessionStore() {    return SessionStoreInMemory.<String>create()        .withLifetime(Duration.ofDays(30));  }
  @Provides  AsyncServlet servlet(SessionStore<String> sessionStore,      @Named("public") AsyncServlet publicServlet, @Named("private") AsyncServlet privateServlet) {    return SessionServlet.create(sessionStore, SESSION_ID, publicServlet, privateServlet);  }

Рассмотрим предоставленные объекты более подробно: - AuthService - логика авторизации и регистрации.

  • Исполнитель - необходим для StaticLoader
  • StaticLoader - загружает статическое содержимое из каталога /site
  • SessionStore - удобное хранилище для информации о сеансах
  • AsyncServlet Сервлет - основной сервлет, объединяющий публичные и приватные сервлеты (для авторизованных и неавторизованных сессий). Как вы можете видеть, благодаря DI, этот сервлет требует только два сервлета без собственных зависимостей Теперь давайте предоставим сервлеты public и private* . AsyncServlet publicServlet - управляет неавторизованными сессиями:
@Provides@Named("public")AsyncServlet publicServlet(AuthService authService, SessionStore<String> store, StaticLoader staticLoader) {  StaticServlet staticServlet = StaticServlet.create(staticLoader, "errorPage.html");  return RoutingServlet.create()      //[START REGION_3]      .map("/", request -> HttpResponse.redirect302("/login"))      //[END REGION_3]      .map(GET, "/signup", StaticServlet.create(staticLoader, "signup.html"))      .map(GET, "/login", StaticServlet.create(staticLoader, "login.html"))      //[START REGION_4]      .map(POST, "/login", request -> request.loadBody()          .then(() -> {            Map<String, String> params = request.getPostParameters();            String username = params.get("username");            String password = params.get("password");            if (authService.authorize(username, password)) {              String sessionId = UUID.randomUUID().toString();
              return store.save(sessionId, "My object saved in session")                  .map($ -> HttpResponse.redirect302("/members")                      .withCookie(HttpCookie.of(SESSION_ID, sessionId)));            }            return staticServlet.serve(request);          }))      //[END REGION_4]      .map(POST, "/signup", request -> request.loadBody()          .map($ -> {            Map<String, String> params = request.getPostParameters();            String username = params.get("username");            String password = params.get("password");
            if (username != null && password != null) {              authService.register(username, password);            }            return HttpResponse.redirect302("/login");          }));}

Давайте подробнее рассмотрим, как мы настраиваем маршрутизацию для сервлетов. Подход ActiveJ напоминает Express. Например, - это запрос на домашнюю страницу для неавторизованных пользователей:

.map("/", request -> HttpResponse.redirect302("/login"))

Метод map(@Nullable HttpMethod method, String path, AsyncServlet servlet) добавляет маршрут к RoutingServlet

  • метод (необязательно) - один из методов HTTP (GET, POSTи т.д.).
  • путь - путь на сервере.
  • servlet определяет логику обработки запроса. Если вам нужно получить некоторые данные из запроса ** во время обработки, вы можете использовать:
  • request.getPathParameter(String key)/request.getQueryParameter(String key) (см. пример использования параметра запроса) предоставить ключ нужного параметра и получить обратно соответствующий String
  • request.getPostParameters() для получения карты всех параметров запроса GET запросы с путями "/login" и "/signup" загружают необходимые HTML страницы. POST запросы с путями "/login" и "/signup" заботятся о логике входа и регистрации соответственно:
.map(POST, "/login", request -> request.loadBody()    .then(() -> {      Map<String, String> params = request.getPostParameters();      String username = params.get("username");      String password = params.get("password");      if (authService.authorize(username, password)) {        String sessionId = UUID.randomUUID().toString();
        return store.save(sessionId, "My object saved in session")            .map($ -> HttpResponse.redirect302("/members")                .withCookie(HttpCookie.of(SESSION_ID, sessionId)));      }      return staticServlet.serve(request);    }))

Обратите внимание на маршрут POST "/login" . serveFirstSuccessful() принимает два сервлета и ждет, пока один из них успешно завершит обработку. Поэтому, если авторизация не удалась, будет возвращен Promise null (AsyncServlet.NEXT), что означает отказ. В этом случае будет создан простой StaticServlet для загрузки errorPage. Успешный вход в систему сгенерирует для пользователя идентификатор сессии и сохранит строку "Мой сохраненный объект в сессии" в хранилище сессий. Также он перенаправит пользователя на "/members"*. Теперь перейдем к следующему сервлету, который обрабатывает авторизованные сессии. AsyncServlet privateServlet - управляет авторизованными сессиями:

@Provides@Named("private")AsyncServlet privateServlet(StaticLoader staticLoader) {  return RoutingServlet.create()      //[START REGION_6]      .map("/", request -> HttpResponse.redirect302("/members"))      //[END REGION_6]      //[START REGION_7]      .map("/members/*", RoutingServlet.create()          .map(GET, "/", StaticServlet.create(staticLoader, "index.html"))          //[START REGION_8]          .map(GET, "/cookie", request ->              HttpResponse.ok200().withBody(wrapUtf8(request.getAttachment(String.class))))          //[END REGION_8]          .map(POST, "/logout", request ->              HttpResponse.redirect302("/")                  .withCookie(HttpCookie.of(SESSION_ID).withPath("/").withMaxAge(Duration.ZERO))));  //[END REGION_7]}

Во-первых, он перенаправляет запросы с домашней страницы на "/members":

.map("/", request -> HttpResponse.redirect302("/members"))

Далее он обрабатывает все запросы, которые идут по пути "/members" :

.map("/members/*", RoutingServlet.create()    .map(GET, "/", StaticServlet.create(staticLoader, "index.html"))    //[START REGION_8]    .map(GET, "/cookie", request ->        HttpResponse.ok200().withBody(wrapUtf8(request.getAttachment(String.class))))    //[END REGION_8]    .map(POST, "/logout", request ->        HttpResponse.redirect302("/")            .withCookie(HttpCookie.of(SESSION_ID).withPath("/").withMaxAge(Duration.ZERO))));

Обратите внимание на путь "/members/*". * - это переменная для следующей части пути. В нем говорится, что этот сервлет будет обрабатывать любой сегмент пути, который идет после "/members/". Например, этот маршрут:

.map(GET, "/cookie", request ->    HttpResponse.ok200().withBody(wrapUtf8(request.getAttachment(String.class))))

это GET-запрос для "/members/cookie" путь. Этот запрос показывает все файлы cookie, сохраненные в сессии.

caution

Запрос может иметь карту вложений, где любое содержимое может быть сопоставлено с некоторым типом, т.е. Струна. По умолчанию запросы не имеют вложений. В данном случае запрос содержит 'cookies' в качестве вложения, которое сопоставлено с типом String . ::: "/members/logout" выводит пользователя из системы, удаляет все куки, связанные с этой сессией, и перенаправляет пользователя на домашнюю страницу. После того, как public и private сервлеты установлены, мы определяем main() метод, который запустит нашу программу запуска:

public static void main(String[] args) throws Exception {  AuthLauncher launcher = new AuthLauncher();  launcher.launch(args);}

Запуск приложения

Если вы хотите запустить пример, клонируйте ActiveJ и импортируйте его как проект Maven. Посмотрите ветку v5.0. Перед запуском примера выполните сборку проекта (Ctrl F9 для IntelliJ IDEA).

Откройте класс AuthLauncher и запустите его метод main() . Затем откройте ваш любимый браузер и перейдите по адресу localhost:8080. Попробуйте зарегистрироваться, а затем войти в систему. Когда вошел в систему, проверьте сохраненные файлы cookie для сессии. Вы увидите следующее содержание: Мой сохраненный объект в сессии. Наконец, попробуйте выйти из системы. Вы также можете попытаться войти в систему с недействительным логином или паролем.