[Spring-Security] 기본 동작과정, 필터 구조

조대훈·2024년 3월 20일
0

스프링 시큐리티

목록 보기
4/4
post-thumbnail

스프링 시큐리티란?

인증Authentication 인가Authorization 에 대한 처리를 위임하는 별도의 프레임 워크이다.
Spring Security인증 인가는 대부분을 Filter 의 흐름에 따라 처리한다. Filter
Dispatcher Servlet 이전에 적용 되므로 가장 먼저 URL 요청을 받는다.

스프링 시큐리티의 동작 과정

스프링 시큐리티 프레임워크가 인증, 인가를 어덯게 처리하는지?

클라이언트가 DispatcherServlet 에 도달하기 전에 Filter에서 요청을 가로채 인증인가 에 대한 처리를 해준다.

  1. 사용자가 로그인 정보 id password 를 로그인 인증 Authentication 요청
  2. AuthenticationFilter 가 정보를 가로채 UsernamePasswordAuthentication Token 을 생성하여 (Authentication 객체) AuthenticationManager 에게 Authentication 전달
  3. AuthenticationManager 인터페이스를 거쳐 Autehntication Provider 에게 정보 전달, 등록된 AuthenticationProvider 를 조회하여 인증 요구 한다.
  4. AutenticationProviderUserDetailService 를 통해 입력받은 (3)의 사용자 정보를 DB에서 조회한다.
    1. .support()메소드가 실행 가능한지 체크
    2. authenticate() 메소드를 통해 DB에 저장된 이용자 정보와 입력한 로그인 정보 비교
      1. DB 이용자 정보 : UserDetailServiceloadUserByUsername() 을 통해 불러옴
      2. 입력 로그인 정보 : (3)에서 받았던 Authentication 객체 UsernameAuthentication Token
    3. 일치하는 경우 Authentication 반환
  5. AuthenticationManagerAuthentication 객체를 AutenticationFilter 로 전달
  6. AuthenticationFilter 는 전달 받은 Authentication 객체를 LoginSuccessHandler 로 전송 하고 SecurityContextHolder 에 담는다.
  7. 성공시 AuthenticationSucesshandle 실패시 AuthenticationFailureHandle 실행

의존성 추가시 보게될 첫 화면

  • 단순히 Spring Security 의존성을 추가함으로 /login 페이지에 해당 화면을 볼수 있다.

서버 빌드시 password 확인이 가능하며 기본 id는 user 이다.
첫 번째 화면에서만 자격증명을 요구하며 세션ID나 토큰 세부 정보를 이용한다.

application.properties 정적 인증 정보 설정

위와 같이 변경이 가능하며 서버를 빌드할 때 마다 비밀번호를 복사-붙여넣기 하지 않아도 된다.

서블렛과 필터

SpringSecurity Filter

스프링 필터 속에는 각기 다른 기능을 가진 필터들을 대거 포함하고 있으며 그 갯수는 자그마치 20개가 족히 넘는다. 주 기능은 유저네임과 비밀번호를 2 단계 안의 인증 객체로 변환하며, 개게가 생성되면 Spring Security Filter 들은 이 요청을 인증 관리자 (authentication manager)
에게 넘긴다.

  1. 인증 권한 부여 수행
  2. 로그인 페이지 표시
  3. HDT 포지션 내 인증 정보 저장 등
  4. Authorization Filter - 엔드 유저가 접근하고자 하는 URL에 접근을 제한
  5. doFilter - 공개 URL인지, 보안 URL 인지 체크 후 접근 허용 혹은 접근 제한

Authentication Manager

이름에서 알수 있듯 이 인증관리자는 실질적인 인증 로직을 관리하는 인터페이스 클래스이다.
인증 관리자가 하는 일은 웹 애플리케이션 안에 어떤 인증 제공자 (authentication provider) 가 존재하는지 확인한다.
특정 요청에 대해 유요한 인증 제공자가 어떤 것인지 확인하는 거이 인증 관리자의 책임이다. 단순히 로그인에 실패해서 응답을 엔드 유저에게 반환하는 것이 아닌 가능한 모든 인증 제공자들을 시도하고 모든 시도에서 실패했을 때 비로소 인증이 실패했다고 응답한다.

Authencitation Providers

실질적인 인증 로직을 정의하는 부분이다. 가령 어떤 데이터베이스나, LDAP 서버에서 혹은 권한 부여 서버나 캐시에서 유저 자격 증명을 할 것인지 서술한다.
웹 애플리케이션 내에 다수의 인증 제공자를 작성할 수 있다. 예를 들어 유저네임과 비밀번호 인증 담당하는 인증 제공자와 별도로 로그인을 하기위핸 OR 매핑을 프레임워크에 활용하게 할수도 있다.

UserDetailsManager, UserDetailsService

대부분의 프로젝트에서 요구하는 공통 로직을 Sprng Security에서 이미 구현해 놓은 인터페이스와 클래스.아이디, 비밀번호 대조에 사용된다. Password Encoder 와 협동하여 작동한다.

Security Context

Spring Security Filter는 생성된 인증 객체를 보안 컨텍스트 안에 저장하데 이를 공간이다. 인증이 성공적이었는지 유무와, 세션 ID 등을 저장한다. 이미 인증정보가 저장 되어 있다면 재 접속 시에 다시 인증요구를 하지 않는다.

시퀀스 플로우

SecurityFilterChain

default Security Configurations 안에는 defaultSecurityFilterChain 클래스가 있는데 HttpSecurity 를 매개변수로 받는다. 위 클래스를 해석 해보면 모든 URL을 보호하게 만들고 있음을 아수 있다.

http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());

이 줄 하나로 모든 경로의 요청이 증명이 되어야 한다.

SecurityFilterChain 적용

@Configuration  
public class ProjectSecurityConfig {  
  
    @Bean  
    @Order(SecurityProperties.BASIC_AUTH_ORDER)  
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {  
        http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());  
        http.formLogin(withDefaults());  
        http.httpBasic(withDefaults());  
        return http.build();  
    }  
}

SecurityFilterChain 수정하기


@Configuration  
public class ProjectSecurityConfig {  
  
    @Bean  
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {  
        http.authorizeHttpRequests((requests) -> requests  
                        .requestMatchers("/myAccount","/myBalance","/myLoans","/myCards").authenticated()  
                        .requestMatchers("/notices","/contact").permitAll())  
                .formLogin(Customizer.withDefaults())  
                .httpBasic(Customizer.withDefaults());  
                // httpBasic() 메소드는 HTTP 기본 인증을 활성화한다. 이것은 사용자 이름과 비밀번호를 사용하여 인증을 수행한다.  
  
        return http.build();  
  
    }  
}

formLogin

  • 사용자가 로그인 할 수 있는 폼을 제공한다.
  • 로그인 성공시 토큰을 발급한다

httpBasic

  • HTTP 기본인증을 사용하여 사용자 인증을 수행한다.
  • 사용자에게 로그인 폼을 표시하지 않고 브라우저에서 사용자 이름과 비밀번호를 직접 입력하도록 한다.
  • 클라이언트와 서버 간에 Base64로 인코딩된 사용자 이름과 비밀번호를 전송한다.

(Customizaer.withDefaults())

  • formLogin()httpBasic() 메소드의 매개 변수로 사용된다.
  • 기본 설정을 사용하여 인증 기능을 설정한다.
  • Customizar 인터페이스르 구현하는 객체를 전달하여 기본 설정을 변경할 수 있다.

SecurityFilterChain모든 요청 거부하기

@Configuration  
public class ProjectSecurityConfig {  
  
    @Bean  
    @Order(SecurityProperties.BASIC_AUTH_ORDER)  
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {  
        http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());  
        http.formLogin(withDefaults());  
        http.httpBasic(withDefaults());  
        return http.build();  
    }  
}

SecurityFilterChain 모든 요청 허용하기

@Configuration  
public class ProjectSecurityConfig {  
  
    @Bean  
    @Order(SecurityProperties.BASIC_AUTH_ORDER)  
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {  
http.authorizeHttpRequests((requests) -> requests.anyRequest().permitAll())  
        .formLogin(withDefaults()).httpBasic(withDefaults()  
        );
    
	}
}  

InMemoryUserDetailsManager 를 사용한 유저 설정 1

UserDetails 를 이용하여 User.build() 하게 되면 InMemoryUserDetailManagerbuild()User 를 전부 생성하게 된다.


@Bean  
public InMemoryUserDetailsManager userDetailService() {  
    UserDetails admin = User.withDefaultPasswordEncoder()  
            .username("admin")  
            .password("12345")  
            .authorities("admin")  
            .build();  
  
  
    UserDetails user = User.withDefaultPasswordEncoder()  
            .username("user")  
            .password("12345")  
            .authorities("user")  
            .build();  
      
    return new InMemoryUserDetailsManager(admin, user);  
}

InMemoryUserDetailsManager 를 사용한 유저 설정 2

방법2에는 사소한 차이가 있는데 바로 PasswordEncoder 관련 메서드를 호출하지 않는 다는 것이다.
비밀번호의 암호화 및 해싱을 수행하지 않는 방법 이다


@Bean  
public InMemoryUserDetailsManager userDetailService() {  
    UserDetails admin = User  
            .withUsername("admin")  
            .password("12345")  
            .authorities("admin")  
            .build();  
  
  
    UserDetails user = User  
            .withUsername("user")  
            .password("12345")  
            .authorities("user")  
            .build();  
  
    return new InMemoryUserDetailsManager(admin, user);  
}

username 을 바로 사용하지 못하는 이유는 첫 번째 파라메터에서는 withDefaultPasswordEncoderwithUsername 을 써야하기 때문이다.

UserDetailsManager 와 UserDetailsService 의 역할

가장 처음에 있는 인터페이스는 UserDetailsService 이다. 이 서비스에는 loadUserByUsename 이라는 이름의 추상 메서드가 있을 것이다. 가령 수행하려는 모든 인증 작업은 브라우저내의 입력된 유저정보, DB내에 있는 저장 정보를 load 하는 것이다. 따라서 loadUsersByUsername 이라는 이름의 단일 추상 메소드를 가진 별도의 인터페이스가 별도로 있는 것이다.

여기서 왜 유저의 이름으로만 로드하는 걸까? 비밀번호 둘 다 쓰면 안되나?

  1. 비밀번호를 불필요하게 네트워크로 전송 해서는 안된다.
    1. 웹 조건 및 SQL문에서 비밀번호를 사용하기 시작하면, 유저의 실제 비밀번호를 데이터베이스 서버에 전송되게 되는데 이는 권장되지 않는 방식이다.
  2. 먼저 유저 세부 정보를 로드 하여, 나중에 비밀번호를 비교하는 논리를 추가하기 위함이다.
    1. 인증이 성공했는지 유무에 따라 별도의 조치를 취할 수 있다.
    2. 이 인터페이스 이후 UserDetailSevice 를 확장하는 또 다른 인터페이스가 있는데 UserDetailsManager이며 유저 세부정보를 관리한다. (생성, 수정,삭제,변경, 등 )위에 보면 다 나와있음

데이터베이스를 다룰 때엔 JdbcUserDetailsManager 를 사용하면 된다. 이와 비슷하게 저장하기 위해 LdapServer 를 이용하는 경우 LdapUserDetailsManager 를 사용하면 된다. 총 이 3가지가 SpringSecurity FrameWork 에서 제공하는 세가지 구현 클래스이다.

자체 인증 로직이 있거나 직접 작성하려는 경우 자체 AuthenticationProvider 를 정의하면 된다.

위의 모든 인터페이스와 클래스는 유저 세부 정보를 저장하고 로드하고, 업데이트, 수정을 하는데 정작 유저 세부 정보는 어떻게 다루지?

UserDetails 의 구현체 중 하나인 User 라는 클래스를 제공한다.

SecurityFilter -> AutenticationManager 구현체 : ProviderManager -> AuthneticationProvider 구현체 : DaoAuthenticationProvider ->UserDetailsManager 구현체 :InMemoryUserDetailsManager

진행한 예제에서는 InMemoryUserDetailsManager 유형의 Bean을 생성했기 때문

profile
백엔드 개발자를 꿈꾸고 있습니다.

0개의 댓글