본문 바로가기

Language/Spring Security

익명 사용자 ­- anonymous()

  • 스프링 시큐리티에서 익명으로 인증된 사용자와 인증되지 않은 사용자 간에 실제 개념적 차이는 없으며 단지 액세스 제어 속성을 구성하는 더 편리한 방법을 제공한다고 볼 수 있음
  • SecurityContextHolder 가 항상 Authentication 객체를 포함하고 null 을 포함하지 않는다는 것을 규칙을 세우게 되면 클래스를 더 견고하게 작성 가능
    • 인증 받은 사용자, 익명 사용자(인증 받지 못한 사용자) 두 개의 객체로 구분
  • 인증 사용자와 익명 인증 사용자를 구분해서 어떤 기능을 수행하고자 할 때 유용할 수 있으며 익명 인증 객체를 세션에 저장하지 않음
    • 인증 사용자, 익명 사용자 모두 시큐리티 컨텍스트에 저장
    • 단, 인증 사용자는 세션에 저장하지만 익명 사용자는 인증 받지 못한 상태를 유지할 필요가 없기 때문세션에 저장하지 않음
  • 익명 인증 사용자의 권한을 별도로 운용 가능
    → 즉, 인증된 사용자가 접근할 수 없도록 구성이 가능

 

익명 사용자 API 및 구조

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
            .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
            .formLogin(Customizer.withDefaults())
            .anonymous(anonymous -> anonymous
                    .principal("guest")
                    .authorities("ROLE_GUEST")
            );
    return http.build();
}
  • AnonymousAuthenticationFilter
    • 현재 접속한 사용자가 인증 받지 못했으면 익명 사용자의 인증 객체를 생성
    • 시큐리티 컨텍스트 안에 저장
  • AnonymousAuthenticationToken
    • anonymousUser (문자열) → principal에 담김
    • ROLE_ANONYMOUS (권한) → authorities에 담김

스프링 MVC에서 익명 인증 사용하기

  • 스프링 MVC가 HttpServletRequest#getPrincipal을 사용하여 파라미터를 해결하는데 요청이 익명일 때 이 값은 null
public String authentication(Authentication authentication){
    if (authentication instanceof AnonymousAuthenticationToken) {
        return "anonymous";
    } else {
        return "null";
    }
}

  • 익명 요청에서 Authentication 을 얻고 싶다면 @CurrentSecurityContext를 사용
  • CurrentSecurityContextArgumentResolver 에서 요청을 가로채어 처리
public String anonymousContext(@CurrentSecurityContext SecurityContext context){
    return context.getAuthentication().getName();
}

AnonymousAuthenticationFilter

  • SecurityContextHolder에 Authentication 객체가 없을 경우(null) 감지하고 필요한 경우 새로운 Authentication 객체로 채움
  1. client GET방식으로 “/index” URL 요청
  2. AnonymousAuthenticationFilter가 받음
    • 이전의 필터들에서 인증 받지 못하고 해당 필터까지 왔을 때 익명 처리
  3. Authentication의 null 유무 확인
    • null이 아닐 경우 인증을 받아 다음 필터로 진행(chain.doFilter)
    • null일 경우 다음 스텝으로 진행
  4. AnonymousAuthenticationToken 생성
    • 기본값 ⇒ anoymousUser, ROLE_ANONYMOUS
  5. SecurityContextHolder → Authentication을 SecurityContext에 설정

IndexController

import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.CurrentSecurityContext;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class IndexController {
    @GetMapping("/")
    public String index(){
        return "index";
    }

    @GetMapping("/loginPage")
    public String login(){
        return "loginPage";
    }

    @GetMapping("/anonymous")
    public String anonymous(){
        return "anonymous";
    }

    @GetMapping("/authentication")
    public String authentication(Authentication authentication){

        if (authentication instanceof AnonymousAuthenticationToken) {
            return "anonymous";
        } else {
            return "null";
        }
    }
    @GetMapping("/anonymousContext")
    public String anonymousContext(@CurrentSecurityContext SecurityContext context){
        return context.getAuthentication().getName();
    }
}

SecurityConfig

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint;

import java.io.IOException;

@EnableWebSecurity
@Configuration
public class SecurityConfig {

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

        http
                .authorizeHttpRequests( auth -> auth
                        .requestMatchers("/anonymous").hasRole("GUEST")
                        .requestMatchers("/anonymousContext","/authentication").permitAll()
                        .anyRequest().authenticated())
                .formLogin(Customizer.withDefaults())
                .anonymous(anonymous -> anonymous
                        .principal("guest")
                        .authorities("ROLE_GUEST")
                );

        return http.build();
    }

    @Bean
    public UserDetailsService userDetailsService(){
        UserDetails user = User.withUsername("user").password("{noop}1111").roles("USER").build();
        return  new InMemoryUserDetailsManager(user);
    }

}
  • .requestMatchers("/anonymous").hasRole("GUEST")
    • /anonymous 주소에 대해서는 권한이 GUEST인 사용자만 접근 가능
  • .requestMatchers("/anonymousContext","/authentication").permitAll()
    • /anonymousContext, /authentication 주소에 대해서 모든 사용자 허용

테스트

  • 비로그인, 로그인 각각 /anonymous 접속
    • 비로그인 시 anonymous 페이지 정상 접속
    • 로그인 시 Whitelabel Error Page
    • SecurityConfig에서 .requestMatchers("/anonymous").hasRole("GUEST")설정으로 /anonymous에 접근 가능 권한이 GUEST로 설정했기 때문

  • 비로그인, 로그인 각각 /authentication 접속
    • 두 가지 모두 null
    • 인증 하지 않았더라도 authentication에는 null이 담기기 때문에 anonymous를 반환받지 못함

  • 비로그인, 로그인 각각 /anonymousContext 접속
    • 비로그인 : guest (Security Context에 저장된 username)
    • 로그인 : user (Security Context에 저장된 username)


정리

  • 인증 사용자와 익명 사용자를 구분하여 어떤 기능을 수행하고자 할 때 유용
    • 인증된 사용자는 접속 하지 못하는 URL → .hasRole(”GUEST”)
  • AnonymousAuthenticationFilter에 도달하기 까지 인증을 받지 못한 경우 익명 사용자 객체 생성
    → AnonymousAuthenticationToken
  • 익명 사용자일 경우 매개변수(Authentication 변수)를 하더라도 변수에는 null이 담기기 때문에 @CurrentSecurityContext 어노테이션을 이용하여 SecurityContext에 접근
  •