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 ConcurrentMapstore = 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); ResponseEntityresponse = 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); ResponseEntityresponse = 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 .