Deep Dive/BACK

Spring Security 분석

MoonJay 2023. 6. 20. 01:16

 

아키텍처

 

 

 

로그인 시작부터 하나하나 분석해보겠습니다.

 

사용자가 로그인을 시도합니다.

<form role="form" method="post" action="/members/login">
....
</form>

로그인을 했을 때 Post방식으로 /members/login으로 요청을 하면 spring security가 아 이것은 로그인을 요청하는 것이구나 인식하고 로그인 절차를 진행합니다.

 

어떻게 로그인 절차로 인식할수있을까요?

 

spring은 기본적으로 action 속성이 /login으로 끝나고 post방식으로 전송되는 경우 해당 요청을 로그인 처리로 간주합니다.

하지만 이것은 사용자가 커스터마이징 할 수 있습니다.

 

http.formLogin().loginProcessingUrl("/custom/login")

이와 같이 설정하면 action="/custom/login" 인경우도 로그인 요청으로 인식합니다 

 

 

로그인 요청을 스프링 시큐리티가 인터셉트합니다.

WebSecurityConfigurerAdapter를 상속한 구성 클래스에서 구현이 이루어집니다.

 

이 클래스에서는 configure(HttpSecurity http)메서드를 오버라이드 하여 보안 필터체인을 구성합니다.HttpSecurity객체는 보안 관련 설정을 구성하는데 사용됩니다.

 

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/login").permitAll() // 로그인 페이지는 인증 없이 접근 가능
                .anyRequest().authenticated() // 그 외의 모든 요청은 인증 필요
                .and()
            .formLogin()
                .loginPage("/login") // 로그인 페이지 경로
                .defaultSuccessUrl("/dashboard") // 로그인 성공 시 이동할 페이지
                .permitAll()
                .and()
            .logout()
                .logoutUrl("/logout") // 로그아웃 URL
                .logoutSuccessUrl("/login") // 로그아웃 후 이동할 페이지
                .permitAll();
    }
    
    // 인증 매니저 구성 등 추가 설정 가능
    // ...
}

 

이와 같이 스프링 시큐리티의 구성 클래스에서 configure(HttpSecurity http) 메서드를 사용하여 필터 체인을 정의하고, 요청을 인터셉트하여 보안 설정을 적용합니다.

 

그럼 로그인을 요청하는 경우엔 어떨까요?

로그인을 요청하면 사용자의 인증 정보를 검사하고 인증을 수행하는 UsernamePasswordAuthenticationFilter가 필요합니다. 일반적으로 http.formLogin()을 호출하여 폼 기반 인증을 활성화할 수 있습니다.

 

 http.formLogin()
                .loginPage("/members/login")
                .loginProcessingUrl("/members/login")
                .defaultSuccessUrl("/")
                .usernameParameter("email")
                .failureUrl("/members/login/error")
                .and()
                .logout()
                .logoutRequestMatcher(new AntPathRequestMatcher
                        ("/members/logout"))
                .logoutSuccessUrl("/")
        ;

 

이렇게 설정하면 스프링 시큐리티는 폼을 통한 로그인 요청을 처리하기 위해 UsernamePasswordAuthenticationFilter를 내부적으로 생성하고 보안 필터 체인에 추가합니다. 즉 로그인 요청은 UsernamePasswordAuthenticationFilter에 의해 처리되는 것입니다. 이 필터는 attemptAuthentication() 메서드에서 이메일/사용자명과 비밀번호를 추출하여 Authentication 객체에 담아서 AuthenticationManager에 전달합니다. attemptAuthentication() 메서드는 인증 작업을 수행하기 전에 사용자가 전달한 인증 정보를 검사하고 처리하는 역할을 합니다.

 

 

인증 작업을 수행합니다

AuthenticationManager는 등록된 AuthenticationProvider를 순차적으로 호출하여 인증 작업을 수행합니다.

일반적으로 UserDtailsService를 구현한 클래스 MemberService도 AuthenticationProvider 중 하나인 DaoAuthenticationProvider가 호출 될 때 사용됩니다.

DaoAuthenticationProvider는 내부적으로 UserDetailsService를 사용해 사용자의 세부정보를 가져온 후 인증 작업을 수행합니다.

 

UserDetailsService는 사용자의 자세한 정보를 로드하는 역할을 수행하는 인터페이스입니다.

loadUserByUsername() 메서드를 구현하여 DB에서 특정 사용자의 UserDetails 객체를 로드합니다.

 @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {

        Member member = memberRepository.findByEmail(email);

        if(member == null){
            throw new UsernameNotFoundException(email);
        }

        return User.builder()
                .username(member.getEmail())
                .password(member.getPassword())
                .roles(member.getRole().toString())
                .build();
    }

다시 말해 AuthenticationManager는 등록된 AuthenticationProvider들을 호출하며, 그 중에는 DaoAuthenticationProvider가 포함 될 수 있고, DaoAuthenticationProvider는 UserDetailsServcie를 활용하여 MemberService의 loadUserByUsername() 메서드를 호출하여 사용자의 정보를 가져오고 UserDetails 객체를 생성하여 반환합니다.

 

이때 loadUserByUsername() 메서드는 내부적으로 호출되는 메소드로서 호출한 곳으로 반환하는 방식이 아닙니다.

이 반환된 UserDetails 객체는 최종적으로 Authentication 객체에 설정됩니다.

 

사용자가 입력한 값과 db에서 가져온 UserDetails의 비교는 DaoAuthenticationProvider에서 진행합니다.

 

비교한 뒤 입력한 값과 db 값이 일치하면 Authenticaiton은 

UsernamePasswordAuthenticationToken을 사용하여 인증된 사용자를 나타냅니다.

이 토큰은 Authentication 객체의 일부로 설정되고 Principal 객체로서 인증된 사용자를 나타냅니다.

 

이 Authentication 객체를 다시 AuthenticationManager를 거쳐 AuthenticationFilter의 securityContext 안에 저장합니다.

 

일반적으로 SecurityContext는 스레드 로컬을 통해 사용됩니다. 즉 각각의 스레드에서 독립적으로 SecurityContext를 유지하여 여러 사용자가 동시에 애플리케이션에 접근할 때, 각 사용자의 인증 정보를 정확하게 유지합니다.

 

spring security는 SecurityContextHolder클래스를 제공하여 SecurityContext에 접근하고 조작할 수 있는 API를 제공합니다. SecurityContextHolder를 통해 현재 사용자의 Authentication 객체를 가져오거나 새로운 객체를 설정할 수 있습니다.

 

 

이 authentication 객체를 통해 권한 설정을 할 수 있습니다.

 

지금까지 정리한 것을 바탕으로 실제 코드를 구성해보았습니다.

 


 

@Configuration 및 @EnableWebSecurity 이 주석은 SecurityConfig 클래스가 Spring Security용 클래스이며

이 애플리케이션에서 웹 보안 기능을 활성화함을 나타냅니다.

http.formLogin() 메서드는 양식기반 인증을 구성하는데 양식 로그인에 대한 로그인 페이지, 성공 URL,

실패 URL 및 기타 관련 설정을 설정합니다.

view 파일에서 /members/login으로 맵핑 값을 보내면 이를 spring security가 가로챕니다.

loginPage("/members/login")

 

 

usernameParameter("email") 메서드는 사용자 이름 필드에 대한 로그인 양식에 사용되는 매개변수 이름을 설정합니다.

이름이 이메일인 양식 필드가 양식 기반 인증 프로세스 중에 사용자 이름 필드로 간주되도록합니다.

여기서는 name 속성이 email로 넘어온 것을 spring security의 사용자 이름 필드에 삽입하는 것입니다.

usernameParameter("email")

 

 

사용자가 로그인을 시도하면 Spring Security의 인증 프로세스가 트리거 되고 loadUserByUsername  메서드가 호출됩니다.

 

이때 memberService는 UserDetailsService 인터페이스를 구현해야합니다.

memberService클래스는 데이터 소스에서 이메일로 사용자를 로드하고 UserDetails 개체를 반환하는 논리를 정의할 수 있는 UserDetailsService 인터페이스를 구현해야합니다.

 

return User.builder() 이하 구문을 통해 DB 소스에서 정보를 가져오고 사용자 정보 및 권한을 포함하는 UserDetails 객체를 생성합니다. userDetails는 인증 및 권한 부여에 사용될 사용자의 세부정보를 담고있습니다.

 

이렇게 반환된 UserDetails 개체는 passwordEncoder를 통해 저장된 비밀번호와 자동으로 비교됩니다.

 

만약 성공적으로 인증이 된다면

인증된 사용자의 세부 정보는 현재 요청에 대한 보안 관련 정보를 나타내는 SecurityContext에 저장됩니다.

SecurityContext는 사용자의 세션과 연결되고 사용자는 보호된 리소스에 액세스 하거나 애플리케이션 내에서 승인된 작업을 수행할 수 있습니다.

 

아래와 같이 각 영역별 권한을 제한합니다.

 

permitAll()에 해당하는 부분은 모든 사람이 접근할 수 있고

.hasRole("ADMIN") 부분은 ADMIN 역할을 가지고 있는 사람만이 접근 할 수 있다는 것입니다.

anyRequest().authenticated()는 그 외의 모든 작업은 권한 승인이 필요하다는 것을 의미합니다.

 

 

 

configure(WebSecurity web) 메소드는 특정 리소스 또는 패턴을 무시하도록 Spring Security를 구성하는데 사용됩니다.

이는 FilterChainProxy의 초기화 중에 호출됩니다.

WebSecurity 개체를 사용해서 CSS,JS,IMG 폴더 아래 있는 이미지파일을 보안 필터에서 제외합니다.

그럼 해당 경로는 권한에 상관없이 모든 이가 접근 가능합니다.

 


이와 같은 과정을 통해 SpringSecurity를 활용하여 인증 및 인가 절차를 간편하게 처리하였습니다.

SecurityConfig를 통해 보안규칙이 정의 된 필터체인을 구축하고 DB에서 가져온 값과 비교하여 인증 절차를 진행하고 Authenticaion 객체에 인증 정보를 저장하며 권한을 부여합니다. 이를 통해 보안에 대한 신뢰성과 안정성을 높일 수 있었습니다.