Spring/Spring Security

[Spring Security] 익명사용자 인증(Anonymous Authentication) / 로그아웃 처리(Handling Logouts)

공대키메라 2022. 7. 31. 22:36

이번 시간에는 Spring Security의 익명 사용자 인증(Anonymous Authentication)에 대해 알아보도록 하겠다.

 

참고로 이건 TMI이긴 한데...

 

Spring Security를 공부하다가 Event 관련 클래스를 상속하는 것들이 있어서 Spring Event를 잘 모르겠어서 공부를 좀 하고 왔기에... Spring Security공부가 중도에 멈췄다... 하여간...

 

참고한 내용은 다음과 같다.

 

출처:

https://docs.spring.io/spring-security/reference/servlet/authentication/anonymous.html

Anonymous Authentication

 

1. 개요

많은 사이트들이 몇개의 URL들보다 모든 URL 들에 사용자가 인증되길 필요로 한다. 

 

이 경우 모두 인증된 resource를 가지는 것 보다는 구체적인 URL들에 대해서 접근 설정 속성을 정의하는 것이 가장 쉽다. 

 

때때로, ROLE_SOMETHING이 기본적으로 필요하다고 하고 오직 이 rule에 특정한 예외만 허락된다고 하는것이 좋을 수 있다. 예를 들어, 로그인이나 로그아웃 그리고 어플리케이션의 home page에서 처럼 말이다. 

 

 또한 전체적으로 filter chain으로부터 이러한 페이지를 생략할 수 있다. 따라서, 접근 제한 체크를 건너 뛰면서 말이다. 

 하지만, 이게 다른 이유에서 필요하지 않을 수 있는데, 특히 인증받은 사용자에게 이 페이지들이 다르게 행동하면 말이다. 

 

이게 익명사용자 인증이 의미하는 것이다. (...?)

 

익명적으로 인증된 사용자와 비인증된 사용자 사이에 실제 개념적으로 차이가 없다는 것을 알아라.

 

Spring Security의 익명 사용자 인증은 당신의 access-control 속성을 설정하기 위핸 더 편리한 방식을 제공할 뿐이다.

 

예를 들어, getCallerPrincipal같은 servlet API를 부르는 것은 실제로 익명사용자 인증 객체가 SecurityContextHolder에 있어도 null을 반환(return)할 것이다.

 

익명 사용자 인증이 유용한 다른 상황들이 있는데,  auditing interceptor가 SecurityContextHolder에게 principal이 주어진 작업에 책임이 있는지 신원을 확인하도록 질문하는 것 처럼 말이다.

 

클래스들은 만약 SecurityContextHolder가 항상 null이 아닌 Authentication 객체를 포함하고 있다면 더 강력하게 쓰일 수 있다. 

 

결국에는 Anonymous Authentication은 인증된 사용자의 반대의 개념으로 구분하기 위해 만들어낸 것이다.

즉, 익명 사용자와 인증 사용자를 구분해서 처리하기 위한 용도로 사용한다. 

편의성을 위해서 말이다.

 

2. 설정(Configuration)

Anonymous authentication 지원은 자동적으로 HTTP Configruation Spring Security 3.0을 사용할 때 제공된다. 그리고 <anonymous> element를 사용해서 customize할 수 있다. 

 

당신은 전통적인 빈 설정을 사용하지 않는다면 여기 묘사된 빈들을 설정할 필요가 없다. 

 

세 개의 클래스들이 함께 anoymous authentication 기능을 제공한다. 

 

Name Description
AnonymousAuthenticationToken Authentication의 구현체이고 익명 principal을 요청하는 GrantedAuthortity를 저장한다. 
AnonymousAuthenticationProvider AnonymousAuthenticationToken들이 받아들여지도록 하기 위해 ProviderManager와 연결된다. 
AnonymousAuthenticationFilter 보통의 인증 메커니즘 후에 연결되고 만약에 존재하는 Authentication이 없다면 SecurityContextHolder에 AnonymousAuthenticatioToken을 자동적으로 추가한다. 

 

3. AuthenticationTrustResolver

anonymous authentication discussion을 자세히 설명하는 것은 AuthenticationTrustResolverImpl의 구현체에 상응하는 AuthenticationTrustResolver 인터페이스이다.

 

이 인터페이스는 isAnonymous(Authentication) 메소드를 제공하는데, 이것은 관심있는 클래스들이 인증 상태의 특별한 형태를 고려하도록 한다. 

 

ExceptionTranslationFilter는 AccessDeninedException을 처리하는 것에서 이 인터페이스를 사용한다. 

 

AccessDeniedException 이 발생하면, 인증은 403 응답을 던지는것 대신에 anonymous type이고, filter 은 대신에 AuthenticationEntryPoint를 시작해서 principal이 적절히 인증할 수 있도록 한다. 

 

이것은 필요한 구별인데, 그렇지 않으면, principal들이 항상 인증 된 것으로 여겨지고 form, basic, digest 혹은 다른 일반 인정 메커니즘을 통해 로그인 할 기회를 얻지를 못한다. 

 

4. Getting Anonymous Authentications with Spring MVC

 

Spring MVC는 그 자신만의 argument resolver를 사용해서 Principal type의 파라미터를 설명한다.

 

이것은 다음과 같은 것을 말한다. 

 

@GetMapping("/")
public String method(Authentication authentication) {
	if (authentication instanceof AnonymousAuthenticationToken) {
		return "anonymous";
	} else {
		return "not anonymous";
	}
}

 

익명의 요청에도 항상 "not anonymous"를 반환할 것이다. 

 

이유는 Spring MVC가 HttpServletRequest의 getPrincipal을 사용해서 파라미터를  resolve하기 때문이다. HttpServletRequest의 getPrincipal은 요청이 anonymous 면 null이다. 

 

anonymous 요청에서 Authentication을 포함하고 싶다면, @CurrentSecurityContext를 대신에 사용해라.

 

Example 1. Use CurrentSecurityContext for Anonymous requests

@GetMapping("/")
public String method(@CurrentSecurityContext SecurityContext context) {
	return context.getAuthentication().getName();
}

 

5. 로그인 시 debugger로 알아보기

 

필자는 매우 간단한 코드만 작성한 후에 Anonymous관련 Filter등등의 클래스에 break point 를 걸어서 확인해 보았다. 

 

SecurityConfig4.java

package springsecuritytutorial.tutorial.security;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@RequiredArgsConstructor
public class SecurityConfig4 {

    private final UserDetailsService userDetailsService;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        http
                .authorizeRequests()
                .anyRequest().authenticated();

        http
                .formLogin();

        return http.build();
    }

}

 

참고로 현재 다음과 같은 filter들이 등록되있는것을 확인할 수 있다.

 

 

우선 AnonymousAuthenticationFilter를 살펴볼 것이다. 

 

 

바로 서버를 실행하고 default 페이지에 접속했는데, SecurityContextHolder에 현재 인증받은 사용자가 있는지를 확인한다. 

 

서버를 실행한 즉시 접속하면 물론 로그인이 되어있지 않기 때문에 현재 SecurityContextHolder에 Security 정보가 없다. 그래서 당연히 null이다. 

 

그러면 이제 Authentication을 createAuthentucation으로 생성한다. 

 

 

그런데... 위를 자세히 보면 AnonymousAuthenticationToken을 발행한다. 

 

 

그리고 그 토큰을 다시 SecurityContextHolder에 등록한다. 

 

Handling Logouts

 

6. Logout Java/Kotlin Configuration

 

HttpSecurity bean을 주입할 때,  logout 능력들은 자동적으로 등록된다. default은 URL /logout으로 접근하는것이 user가 log out 하도록 할 것이다. 다음과 같은 방식으로 말이다.

 

  • Invalidating the HTTP Session
  • Cleaning up any RememberMe authentication that was configured
  • Clearing the SecurityContextHolder
  • Redirect to /login?logout

 

하지만, 로그인 능력들을 설정하는것과 비슷하게, 또한 당신의logout 요구에 더 맞춰 customize하기 위해 다양한 옵션을 가진다. 

 

public SecurityFilterChain filterChain(HttpSecurity http) {
    http
        .logout(logout -> logout                                                
            .logoutUrl("/my/logout")                                            
            .logoutSuccessUrl("/my/index")                                      
            .logoutSuccessHandler(logoutSuccessHandler)                         
            .invalidateHttpSession(true)                                        
            .addLogoutHandler(logoutHandler)                                    
            .deleteCookies(cookieNamesToClear)                                  
        )
        ...
}

 

1. logout 지원을 제공한다.

 

2. logout 을 발생할 URL을 설정한다. 만약 CSFR 보호가 가능하다면, 그리고 나서 요청이 POST 되야만 한다. 

 

3. logoutSuccessUrl을 통해 logout이 발생한 후에 redirect할 URL을 설정한다. 기본은 /login?logout이다. 

 

4. custom LogoutSuccessHandler을 구체화한다. 만약 구체화된다면(specified), logoutSuccessUrl()은 무시된다. 

 

5. logout 시에 HttpSession을 무효화할지 아닐지 정한다. 기본적으로 true면, SecurityContextLogoutHandler를 아래에 설정한다. 

 

6. LogoutHandler를 더한다. SecurityContextLogoutHandler가 기본적으로 마지막 LogoutHandler로 추가된다. 

 

7. logout 성공시에 지워질 쿠키들의 이름을 정할 수 있다. 이것은  명확하게 CookieClearingLogoutHandler의 shortcut이다.

 
 
사실 위에 제공한 예시 코드들은 코드 어시스트를 받아보면 전~부 편하게, 직관적으로도 확인할 수 있는 내용들이 많다.


옆에 자세히 보면 Logout ~~ 하고 적힌 것들이 있는데 이것만 봐도... logout 설정을 하고 싶을 때 뭘 쓰는지 대강 알 수 있기는 하다.

 
 
일반적으로, logout 기능을 customize하기 위해서, LogoutHandler를 추가할 수 있고 LogoutSuccessHandler의 구현체들을 추가할 수 있다. 
 
많은 보통의 시나리오들을 위해, 이 핸들러들이 fluent API를 사용할 때 내재되어서 적용된다.

 

7. LogoutHandler

일반적으로 LogoutHandle 구현체들은 로그아웃 handling에 참여할 수 있는 클래스들을 나타낸다.

 

그 구현체들은 필요한 지우기(clean-up)를 수행하기 위해 호출되도록 기대된다. 

 

그것들이 예외를 던질 수 없는것처럼 말이다. 다양한 구현체들이 제공되는데, 다음과 같다.

 

 

찾아보니 이렇게 LogoutHandler를 구현한 구현체들을 확인할 수 있었다.

 

 

Please see Remember-Me Interfaces and Implementations for details.

 

LogoutHandler 구현체를 직접적으로 제공하는 것 대신, fluent API는 또한 각각의 LogoutHandler 구현체들을 제공하는 shortcut을 제공한다. 예를 들어, deleteCookie()는 CookieClearingLogoutHandler의 shortcut이다. 

 

8. LogoutSuccessHandler

LogoutSuccessHandler는 LogoutFilter에 의해 성공적인 Logout후에 호출된다. 적절한 목적지를 향해 redirection이나 forwarding하는것을 다루기 위해 말이다. 

 

interface가 거의 LogoutHandler랑 비슷하지만, exception이 발생할 수 있다는 점을 유의해라.

 

다음 구현체들이 제공된다.

 

위에서 언급했듯이, SimpleUrlLogoutSuccessHandler를 직접적으로 구현할 필요가 없다. 대신에, fluent API가 logoutSuccessUrl()을 세팅해서 shortcut을 제공한다. 

 

이것은 SimpleUrlLogoutSuccessHandler를 미리 세팅할 것이다. 제공된  URL은 로그아웃 발생 시 redirect될 것이다. default URL은 /login?logout이다. 

 

HttpStatusReturningLogoutSuccesHandler는 REST API 형태의 시나리오에서 흥미로울 수 있다. 

 

성공적인 logout 다음에 URL을 redirect 하는 것 대신에, 이 LogoutSuccessHandler는 평범한 HTTP 상태코드가 돌려받도록 한다. 만약 설정이 안됐다면 상태 코드 200이 반환될 것이다. 

 


 

LogoutSuccessHandler를 어떻게 사용하는지 보면 뭐... 그냥 치면 바로 다 나온다.

 

 

 

그냥 읽어보면 어떻게 사용하는지, 그리고 parameter를 통해 뭐를 꺼낼 수 있고 어떻게 하면 되는지 다~ 알거같다. 

(아니 스프링 너무 친절한거 아니냐옹? 코드로 어떻게 하는지 모르겠다고? 다른데 찾아보세요!)


이번 시간에는 anonymous authentication과 Logout handling에 대해 알아보았다.

 

그렇게 특별한 것은 없었고 다른 내용에 비해 상대적으로 이해가 쉬운 편이라고 생각한다.