Security 의존성만으로 모든 요청에 인증이 필요한 이유!

이원찬·2024년 8월 2일
0

Spring

목록 보기
11/13

스프링 시큐리티 의존성을 설치하기만해도 모든 요청에 인증이 요구된다.

왜 일까?

spring-boot-starter-security 의존성이 추가되면

자동으로 SecurityAutoConfigurationDefaultAuthenticationEventPublisher 가 Bean으로 등록된다.

아래는 시큐리티 자동 설정 클래스인 SecurityAutoConfiguration 의 내용이다.

SecurityAutoConfiguration

@AutoConfiguration(
    before = {UserDetailsServiceAutoConfiguration.class}
)
@ConditionalOnClass({DefaultAuthenticationEventPublisher.class})
@EnableConfigurationProperties({SecurityProperties.class})
@Import({SpringBootWebSecurityConfiguration.class, SecurityDataConfiguration.class})
public class SecurityAutoConfiguration {
    public SecurityAutoConfiguration() {
    }

    @Bean
    @ConditionalOnMissingBean({AuthenticationEventPublisher.class})
    public DefaultAuthenticationEventPublisher authenticationEventPublisher(ApplicationEventPublisher publisher) {
        return new DefaultAuthenticationEventPublisher(publisher);
    }
}

위 퍼블리셔는

사용자가 로그인을 시도하거나 실패했을 때 발생하는 이벤트들

  • BadCredentialsException
  • UsernameNotFoundException
  • AccountExpiredException

등 을 처리하여 로깅하거나, 특정 로직을 수행하는 리스너들에게 알리는 데 사용된다.

또한 @ConditionalOnMissingBean() 의 어노테이션은

인자로 들어온 빈 객체가 존재 하지 않으면 아래 설정을 따른다는 것이고

만약 인자로 있는 AuthenticationEventPublisher 를 우리가 직접 구현한다면 우리가 이벤트 처리등을 커스텀하게 처리 가능하다.

설정을 확장시킨 @Import 어노테이션에 존재하는 SpringBootWebSecurityConfiguration 을 살펴보자

@Import 어노테이션으로 줬기 때문에 SpringBootWebSecurityConfiguration 설정을 확장한다는 뜻이다.

기본설정들을 보자

SpringBootWebSecurityConfiguration

@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnWebApplication(
    type = Type.SERVLET
)
class SpringBootWebSecurityConfiguration {

    ...

    @Configuration(
        proxyBeanMethods = false
    )
    @ConditionalOnDefaultWebSecurity
    static class SecurityFilterChainConfiguration {
        SecurityFilterChainConfiguration() {
        }

        @Bean
        @Order(2147483642)
        SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
            http.authorizeHttpRequests((requests) -> {
                ((AuthorizeHttpRequestsConfigurer.AuthorizedUrl)requests.anyRequest()).authenticated();
            });
            http.formLogin(Customizer.withDefaults());
            http.httpBasic(Customizer.withDefaults());
            return (SecurityFilterChain)http.build();
        }
    }
}

정말길다…

여튼 가장 중요한 defaultSecurityFilterChain 메서드 를 살펴보자

  • SecurityFilterChain 스프링 시큐리티의 필터 체인을 정의한다. 필터체인이란? : 필터 체인은 요청을 가로채고, 인증 및 인가 등의 보안 작업을 수행하는것
  • HttpSecurity http: HTTP 요청 보안을 설정하는 객체이다.
    requests.anyRequest()).authenticated()
    
    이 코드 때문에 모든 요청에 인증이 걸리는것!
  • authorizeHttpRequests: 모든 HTTP 요청을 인증된 사용자만 접근할 수 있도록 설정
  • formLogin(Customizer.withDefaults()): 기본 폼 로그인을 활성화, 사용자가 로그인 페이지를 통해 인증가능함 이 것 때문에 로그인 UI 창이 나오는것!
  • httpBasic(Customizer.withDefaults()): 기본 HTTP 기본 인증을 활성화. 이는 브라우저 팝업을 통해 사용자 이름과 비밀번호를 입력하는 방식!

즉 브라우저에 로그인 폼이 나타나고 모든 요청이 인증이 필요하게 된것은 이 설정들 때문이다!

UserDetailsServiceAutoConfiguration

시큐리티 자동설정 중 하나인 UserDetailsServiceAutoConfiguration 설정파일을 살펴보자

@AutoConfiguration
@ConditionalOnClass({AuthenticationManager.class})
@Conditional({MissingAlternativeOrUserPropertiesConfigured.class})
@ConditionalOnBean({ObjectPostProcessor.class})
@ConditionalOnMissingBean(
    value = {AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class, AuthenticationManagerResolver.class},
    type = {"org.springframework.security.oauth2.jwt.JwtDecoder"}
)
public class UserDetailsServiceAutoConfiguration {
...
		@Bean
    public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties, ObjectProvider<PasswordEncoder> passwordEncoder) {
        SecurityProperties.User user = properties.getUser();
        List<String> roles = user.getRoles();
        return new InMemoryUserDetailsManager(new UserDetails[]{User.withUsername(user.getName()).password(this.getOrDeducePassword(user, (PasswordEncoder)passwordEncoder.getIfAvailable())).roles(StringUtils.toStringArray(roles)).build()});
    }

위 코드에서는 시큐리티가 인메모리에 새로운 User와 새로운 역할을 부여해서 InMemoryUserDetailsManager 에 등록하는 모습이다.

위에서 실행 때마다 랜덤한 비밀번호가 로그에 찍혔던 이유는 이 때문이다.

또한

@ConditionalOnMissingBean(
    value = {AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class, AuthenticationManagerResolver.class},

ConditionalOnMissingBean 어노테이션으로 인해 각 클래스들이 존재하지 않으면 아래 코드들이 적용되는 것이도

만약 UserDetailsService 을 상속받아 재정의한다면 커스텀 할수 있을 것이다.

또는 AuthenticationManager, AuthenticationProvider 를 상속받아 재정의 한다면 인증 방식을 커스텀 할수 있을것이다.

ex) jwt 를 이용한 Rest API 구조에서

AuthenticationManager, AuthenticationProvider, UserDetailsService

중요한 위 세가지 클래스를 살펴보자

일단 스프링 시큐리티의 아키텍처 구조를 보자

모든 요청은 필터 체인이 가로채 간다. 만약 Security를 추가한다면 자동으로 필터체인을 등록하는데 등록된 필터체인은 위에서 봤던 defaultSecurityFilterChain 필터체인이다.

AuthenticationManager

AuthenticationManager는 여러개의 provider (공급자) 들을 등록 할 수 있는 코디네이터? 조정자? 정도로 생각하면된다.

AuthenticationProvider

AuthenticationProvider는 특정 유형의 인증을 처리한다.

AuthenticationProvider는 인터페이스이기 때문에 두가지 함수만 열려있다.

우리는 이 AuthenticationProvider 인터페이스를 implements 하여 커스텀 가능하다!

  • authenticate 요청에 대한 인증을 수행하는 함수
  • supports 이 provider가 지정된 인증 유형을 지원하는지 확인하는 함수

샘플 프로젝트에서 사용하고 있는 인터페이스의 중요한 구현 중 하나는 UserDetailsService에서 사용자 세부 정보를 검색하는 DaoAuthenticationProvider 이다.

UserDetailsService

UserDetailsService는 인터페이스로

대부분의 케이스에서 AuthenticationProvider는 데이터베이스에서 자격을 증명하고 유효성 검사를 한다고 한다.

따라서 Spring 에서는 자격 증명 후 유효성 검사 로직의 함수를 단일 함수로 제공하기로 하였다.

  • loadUserByUsername 사용자 이름을 매개변수로 받아들이고 사용자 ID 객체를 반환한다. 만약 이메일로 로그인을 진행한다면 이메일 관련 로직으로 구현하면 된다.

참고 자료
https://ict-nroo.tistory.com/118
https://www.toptal.com/spring/spring-security-tutorial
https://sjh9708.tistory.com/170
https://www.youtube.com/watch?v=KxqlJblhzfI
https://velog.io/@dh1010a/Spring-Spring-Security를-이용한-로그인-구현-스프링부트-3.X-버전-1

profile
소통과 기록이 무기(Weapon)인 개발자

0개의 댓글