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 클래스는 데이터베이스의 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<String, User> store = new ConcurrentHashMap<>(256);
}
이 서비스를 사용하여 데이터베이스에서 사용자 이름(로그인)으로 사용자를 검색하고 Map에 내부적으로 저장할 수 있습니다.
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 를 사용합니다 .
@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. 웹 Security 구성
애플리케이션에서 초점을 맞출 두 가지 간단한 작업인 로그인과 로그아웃이 있습니다. 먼저 사용자가 기본 HTTP 인증을 사용하여 인증할 수 있도록 MVC 구성 클래스를 설정해야 합니다 .
@Configuration
@EnableWebSecurity
public class MvcConfiguration {
@Autowired
private CustomLogoutHandler logoutHandler;
@Bean
public SecurityFilterChain filterChain(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();
return http.build();
}
// further configuration
}
위의 구성에서 주목해야 할 중요한 부분은 addLogoutHandler 메소드입니다. 로그아웃 처리가 끝나면 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);
}
}
보시다시피 로그아웃 방법을 재정의하고 사용자 캐시에서 지정된 사용자를 간단히 제거합니다.
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 메소드 를 통해 사용자를 인증합니다.
- 이제 검색된 사용자 데이터 및 언어 값을 확인할 수 있습니다.
- 결과적으로 사용자가 이제 캐시에 있어야 함을 확인할 수 있습니다.
- 다시 말하지만 언어 Endpoints을 누르고 세션 쿠키를 사용하여 사용자 데이터를 확인합니다.
- 마지막으로 사용자 로그아웃을 확인합니다.
두 번째 테스트에서는 로그아웃할 때 사용자 캐시가 정리 되는지 확인합니다 . 로그아웃 처리기가 호출되는 순간입니다.
@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);
}
다시, 단계별로:
- 이전과 마찬가지로 캐시가 비어 있는지 확인하는 것으로 시작합니다.
- 그런 다음 사용자를 인증하고 사용자가 캐시에 있는지 확인합니다.
- 다음으로 로그아웃을 수행하고 사용자가 캐시에서 제거되었는지 확인합니다.
- 마지막으로 언어 Endpoints에 도달하려는 시도는 401 HTTP 무단 응답 코드로 나타납니다.
5. 결론
이 예제에서는 Spring의 LogoutHandler 인터페이스 를 사용하여 사용자 캐시에서 사용자를 제거하기 위한 사용자 정의 로그아웃 핸들러를 구현하는 방법을 배웠습니다 .
언제나처럼 기사의 전체 소스 코드는 GitHub에서 확인할 수 있습니다 .