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

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

· 6 мин. чтения

1. Обзор

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

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

2. Обработка запросов на выход

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

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

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

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

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

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

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

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

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

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 сопоставляется с таблицей пользователей в нашей базе данных:

@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<String, User> 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());
}

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

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
}

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

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);
}
}

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

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

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

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

ResponseEntity<String> 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<String>(requestHeaders), String.class);
assertThat(response.getBody()).contains("english");

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

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

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

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

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

ResponseEntity<String> 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<String>(requestHeaders), String.class);
assertThat(response.getStatusCode()
.value()).isEqualTo(200);

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

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

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

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

5. Вывод

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

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