Рубрики
Без рубрики

Пользовательский обработчик выхода Spring Security

Узнайте, как реализовать пользовательский обработчик выхода из системы с помощью Spring Security.

Автор оригинала: baeldung.

1. Обзор

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

Именно для этой цели Spring предоставляет интерфейс LogoutHandler , и в этом уроке мы рассмотрим, как реализовать наш собственный пользовательский обработчик выхода.

2. Обработка Запросов На Выход Из Системы

Каждое веб-приложение, которое регистрирует пользователей, должно когда-нибудь выйти из системы. Обработчики безопасности Spring обычно контролируют процесс выхода из системы . В принципе, у нас есть два способа обработки выхода из системы. Как мы увидим, один из них реализует интерфейс LogoutHandler .

2.1. Интерфейс LogoutHandler

Интерфейс LogoutHandler имеет следующее определение:

public interface LogoutHandler {
    void logout(HttpServletRequest request, HttpServletResponse response,Authentication authentication);
}

В наше приложение можно добавить столько обработчиков выхода из системы, сколько нам нужно. Единственным требованием для реализации является то, что никакие исключения не выбрасываются . Это происходит потому, что действия обработчика не должны нарушать состояние приложения при выходе из системы.

Например, один из обработчиков может выполнить некоторую очистку кэша, и его метод должен завершиться успешно. В учебном примере мы покажем именно этот вариант использования.

2.2. Интерфейс LogoutSuccessHandler

С другой стороны, мы можем использовать исключения для управления стратегией выхода пользователя из системы. Для этого у нас есть интерфейс LogoutSuccessHandler и метод onLogoutSuccess . Этот метод может вызвать исключение для установки перенаправления пользователя в соответствующее место назначения.

Кроме того, невозможно добавить несколько обработчиков при использовании LogoutSuccessHandler type , поэтому для приложения существует только одна возможная реализация. Вообще говоря, получается, что это последний пункт стратегии выхода.

3. Интерфейс LogoutHandler на практике

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

Давайте начнем с файла application.properties , который содержит свойства подключения к базе данных для нашего примера приложения:

spring.datasource.url=jdbc:postgresql://localhost:5432/test
spring.datasource.username=test
spring.datasource.password=test
spring.jpa.hibernate.ddl-auto=create

3.1. Настройка Веб-приложения

Затем мы добавим простую сущность User , которую будем использовать для входа в систему и поиска данных. Как мы видим, класс User сопоставляется с таблицей users в нашей базе данных:

@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @Column(unique = true)
    private String login;

    private String password;

    private String role;

    private String language;

    // standard setters and getters
}

Для целей кэширования нашего приложения мы реализуем службу кэширования, которая использует ConcurrentHashMap внутренне для хранения пользователей:

@Service
public class UserCache {
    @PersistenceContext
    private EntityManager entityManager;

    private final ConcurrentMap store = new ConcurrentHashMap<>(256);
}

Используя этот сервис, мы можем извлечь пользователя по имени пользователя (логину) из базы данных и сохранить его внутри нашей карты:

public User getByUserName(String userName) {
    return store.computeIfAbsent(userName, k -> 
      entityManager.createQuery("from User where login=:login", User.class)
        .setParameter("login", k)
        .getSingleResult());
}

Кроме того, можно выселить пользователя из магазина. Как мы увидим позже, это будет основное действие, которое вызовет наш logouthandler:

public void evictUser(String userName) {
    store.remove(userName);
}

Для получения пользовательских данных и языковой информации мы будем использовать стандартный контроллер Spring:

@Controller
@RequestMapping(path = "/user")
public class UserController {

    private final UserCache userCache;

    public UserController(UserCache userCache) {
        this.userCache = userCache;
    }

    @GetMapping(path = "/language")
    @ResponseBody
    public String getLanguage() {
        String userName = UserUtils.getAuthenticatedUserName();
        User user = userCache.getByUserName(userName);
        return user.getLanguage();
    }
}

3.2. Настройка веб-безопасности

Есть два простых действия, на которых мы сосредоточимся в приложении — вход и выход. Во-первых, нам нужно настроить наш класс конфигурации MVC, чтобы позволить пользователям аутентифицироваться с помощью Basic HTTP Auth:

@Configuration
@EnableWebSecurity
public class MvcConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomLogoutHandler logoutHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.httpBasic()
            .and()
                .authorizeRequests()
                    .antMatchers(HttpMethod.GET, "/user/**")
                    .hasRole("USER")
            .and()
                .logout()
                    .logoutUrl("/user/logout")
                    .addLogoutHandler(logoutHandler)
                    .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler(HttpStatus.OK))
                    .permitAll()
            .and()
                .csrf()
                    .disable()
                .formLogin()
                    .disable();
    }

    // further configuration
}

Важной частью, которую следует отметить из приведенной выше конфигурации, является метод add Logout Handler . Мы проходим и запускаем наш CustomLogoutHandler в конце обработки выхода из системы . Остальные настройки настраивают базовую аутентификацию HTTP.

3.3. Пользовательский Обработчик выхода из системы

Наконец, и это самое главное, мы напишем наш собственный обработчик выхода из системы, который обрабатывает необходимую очистку пользовательского кэша:

@Service
public class CustomLogoutHandler implements LogoutHandler {

    private final UserCache userCache;

    public CustomLogoutHandler(UserCache userCache) {
        this.userCache = userCache;
    }

    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, 
      Authentication authentication) {
        String userName = UserUtils.getAuthenticatedUserName();
        userCache.evictUser(userName);
    }
}

Как мы видим, мы переопределяем метод logout и просто выселяем данного пользователя из пользовательского кэша.

4. Интеграционное тестирование

Теперь давайте проверим функциональность. Для начала нам нужно убедиться, что кэш работает так, как задумано — то есть загружает авторизованных пользователей в свое внутреннее хранилище :

@Test
public void whenLogin_thenUseUserCache() {
    assertThat(userCache.size()).isEqualTo(0);

    ResponseEntity response = restTemplate.withBasicAuth("user", "pass")
        .getForEntity(getLanguageUrl(), String.class);

    assertThat(response.getBody()).contains("english");

    assertThat(userCache.size()).isEqualTo(1);

    HttpHeaders requestHeaders = new HttpHeaders();
    requestHeaders.add("Cookie", response.getHeaders()
        .getFirst(HttpHeaders.SET_COOKIE));

    response = restTemplate.exchange(getLanguageUrl(), HttpMethod.GET, 
      new HttpEntity(requestHeaders), String.class);
    assertThat(response.getBody()).contains("english");

    response = restTemplate.exchange(getLogoutUrl(), HttpMethod.GET, 
      new HttpEntity(requestHeaders), String.class);
    assertThat(response.getStatusCode()
        .value()).isEqualTo(200);
}

Давайте разложим шаги, чтобы понять, что мы сделали::

  • Во-первых, мы проверяем, что кэш пуст
  • Далее мы аутентифицируем пользователя с помощью метода with Basic Auth
  • Теперь мы можем проверить полученные пользовательские данные и языковое значение
  • Следовательно, мы можем проверить, что пользователь теперь должен быть в кэше
  • Опять же, мы проверяем пользовательские данные, нажимая на конечную точку языка и используя файл cookie сеанса
  • Наконец, мы проверяем выход пользователя из системы

В нашем втором тесте мы проверим, что кэш пользователя очищается при выходе из системы . Именно в этот момент будет вызван обработчик выхода из системы:

@Test
public void whenLogout_thenCacheIsEmpty() {
    assertThat(userCache.size()).isEqualTo(0);

    ResponseEntity response = restTemplate.withBasicAuth("user", "pass")
        .getForEntity(getLanguageUrl(), String.class);

    assertThat(response.getBody()).contains("english");

    assertThat(userCache.size()).isEqualTo(1);

    HttpHeaders requestHeaders = new HttpHeaders();
    requestHeaders.add("Cookie", response.getHeaders()
        .getFirst(HttpHeaders.SET_COOKIE));

    response = restTemplate.exchange(getLogoutUrl(), HttpMethod.GET, 
      new HttpEntity(requestHeaders), String.class);
    assertThat(response.getStatusCode()
        .value()).isEqualTo(200);

    assertThat(userCache.size()).isEqualTo(0);

    response = restTemplate.exchange(getLanguageUrl(), HttpMethod.GET, 
      new HttpEntity(requestHeaders), String.class);
    assertThat(response.getStatusCode()
        .value()).isEqualTo(401);
}

Опять же, шаг за шагом:

  • Как и прежде, мы начинаем с проверки того, что кэш пуст
  • Затем мы аутентифицируем пользователя и проверяем, что он находится в кэше
  • Далее мы выполняем выход из системы и проверяем, что пользователь был удален из кэша
  • Наконец, попытка попасть в конечную точку языка приводит к 401 HTTP-коду несанкционированного ответа

5. Заключение

В этом уроке мы узнали, как реализовать пользовательский обработчик выхода из системы для удаления пользователей из пользовательского кэша с помощью интерфейса Spring LogoutHandler .

Как всегда, полный исходный код статьи доступен на GitHub .