WEB

스프링시큐리티 기본동작 원리

피프밍 2024. 1. 6. 18:48

이글은 예전에 토이프로젝트에서 스프링 시큐리티를 사용했었는데

그때 마크다운으로 정리해놨던 내용이다.

외주를 맡긴 회사는 스프링 시큐리티를 사용하는데

다중 서버 환경에서 기존 SSO(Single Sign On) 가 제대로 동작하지않는 문제가 있었다.

그런데 어디가 문제인지, 어떻게 수정해야될지 모르겠다고 해서 대신 작업을 해줬다.

이 글을 다시 한번 슥 읽고 그쪽 소스를 보니 완전 낯설지는 않아서 수월하게 처리해줬다.

이럴 때 기록이 얼마나 중요한지 깨닫는다.. (근데 너무 힘들어서.. 안하는게 문제)

그래서 여기에 다시 기록해둔다.


여기저기 찾아서 스프링시큐리티를 쓰긴 썼는데

난 스프링 시큐리티 설정만 했는데..아이디와 비밀번호를 입력하면 언제 인증이 되는걸까?

이런 궁금증으로 스프링 시큐리티의 인증 흐름에대해서 알아보게 됐다.

이 글은..

화면에 아이디와 비밀번호를 입력하고 로그인 버튼을 눌렀을때

스프링 시큐리티는 언제 어떻게 확인을 하는지 알아보기위해서 작성한 글입니다.

시큐리티 설정부터 시작해서 스프링 시큐리티 내부 클래스들을 따라가면서

어떤 과정을 거치는치 알아봅니다.

스프링 시큐리티 Form Login 공식문서 :

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

스프링 시큐리티 기본인증 공식문서:

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

이 두 문서를 참고하면 좋을것같습니다.

여기에 나오는 다이어그램같은것들이 스프링시큐리티 검색하면 숱하게 나오는데

사실 다이어그램만 보고 이해가 잘 안돼서 하나하나 따라가본겁니다..^^;

스프링 시큐리티 설정

스프링시큐리티5 이전에는 WebSecurityConfigurerAdapter라는 클래스를 상속받아서 설정을 했지만

스프링시큐리티5 부터는 SecurityFilterChain을 리턴하는 빈을 정의해서 필터를 설정할수있다.

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
                .anyRequest().permitAll()
                .and()
                .formLogin()
                .loginPage("/signIn")
                .loginProcessingUrl("/loginProc")
                .and().build();
    }

이렇게 설정하면 .formLogin()을 통해 form기반의 인증이 활성화됩니다.

이를 통해 사용자명과 비밀번호를 form을 통해 제출해서 인증을 받을수있습니다.

인증이 필요한 페이지에 접근시 .loginPage("/signIn")으로인해 로그인페이지로 이동하고

.loginProcessingUrl("/loginProc")에 맞춰

로그인 form 제출시 action값으로 action="/loginProc"을 설정하면 됩니다.

이제 스프링시큐리티 필터는 loginProcessingUrl에 요청이 들어왔으므로 인증절차를 진행하게됩니다.

스프링 시큐리티 인증 절차

위에서 .formLogin() 설정하면 form기반의 인증이 활성화된다고 했는데

그때 사용되는 필터중 하나가 UsernamePasswordAuthenticationFilter입니다.

여기서부터 인증 절차들을 알아보겠습니다.

UsernamePasswordAuthenticationFilter

이 필터의 부모 클래스가 AbstractAuthenticationProcessingFilter인데

이 클래스의 doFilter메소드를 확인해보면

doFilter

try 문에서 attemptAuthentication를 호출하고,

예외발생없이 성공적으로 코드가 진행되면 successfulAuthentication을 호출하고있습니다.

attemptAuthentication메소드는 UsernamePasswordAuthenticationFilter클래스에 정의돼있습니다.

attemptAuthentication

이 메소드에선 UsernamePasswordAuthenticationTokenunauthenticated(username,password);을 통해서

아직 인증되지않은 토큰 authRequest를 생성하고

return문에서 AuthenticationManager인스턴스의 authenticate()를 호출하면서 토큰을 전달합니다.

AuthenticationManager

AuthenticationManagerauthenticate() 하나만을 가진 인터페이스인데

구현체로는 주로 ProviderManager가 사용됩니다.

ProviderManager
ProviderManager.authenticate()

구현체의 authenticate()코드를 확인해보면

for문을 통해 AuthenticationProvider에게 또다시 인증(authenticate())을 위임합니다.

그리고

if (result != null) {
    copyDetails(authentication, result);
    break;
}

코드를 보면 break문이 있는걸로 봐서

여러 AuthenticationProvider중 하나만 인증에 성공해도 바로 인증을 끝내는걸 알수있습니다.

그럼 이제 AuthenticationProvider는 어떻게 인증을 하는지 알아보겠습니다

AuthenticationProvider는 인터페이스로

인증이 가능한지 확인하는 메소드와 인증하는 메소드 2개를 가지고있습니다.

저의 관심사는 인증하는 메소드 이므로 authenticate()에 대해서 알아보겠습니다.

AuthenticationProvider

이 인터페이스는 위에서 반복문을 통해 유추할수있듯이 아주 많은 구현체를 가지고있습니다.

많은 AuthenticationProvider구현체들

이중에 하나만 인증에 성공해도 인증은 성공한것으로 간주하는데

그 중 알아볼 구현체는 DaoAuthenticationProvider입니다.

DaoAuthenticationProvider

이 클래스는 AbstractUserDetailsAuthenticationProvider를 부모클래스로 가지고있고

AbstractUserDetailsAuthenticationProvider

이 클래스에서 AuthenticationProvider를 구현하고있는 구조입니다.

그럼 당연히 authenticate()를 Override하고있을텐데

사용자정보를 가져오는것과 사용자가 맞는지 인증 하는것 두개로 나눠서 확인해보겠습니다

먼저 사용자 정보를 가져오는 코드

authenticate1

캐시에서 username을 통해서 UserDetails를 가져오고 만약 null이라면

retrieveUser를 호출해서 UserDetails를 가져오고있습니다.

retrieveUser는 자식클래스였던 DaoAuthenticationProvider에 정의돼있습니다.

retrieveUser

코드를 보시면 UserDetailsServiceloadUserByUsername을 호출해서

UserDetails정보를 가져오는것을 알수있습니다

UserDetailsService

UserDetailsServiceloadUserByUsername 하나만을 가진 인터페이스인데

그래서 만약 아이디와 비밀번호를 통한 인증을 사용하기위해서는

UserDetailsService의 구현이 필수입니다.

UserDetailsServiceImpl

loadUserByUsername을 Override하고 프로젝트에서 사용하는 db에서 사용자 정보를 가져옵니다.

그리고 그 사용자 정보를 리턴하는 코드입니다.

User인스턴스에는 사용자의 id, 암호화된 비밀번호가 들어있습니다.

그럼 사용자정보를 가져오는 코드를 확인했으니

authenticate()에서 사용자가 맞는지 인증 하는코드를 확인해보겠습니다.

authenticate2

사용자정보를 가져오는 코드에서 바로 이어지는 코드입니다.

처음 preAuthenticationChecks를 통해 계정이 잠긴 계정인지, 만료되진 않았는지 등을 확인합니다.

그리고 additionalAuthenticationChecks메소드를 통해서 인증이 진행됩니다.

해당메소드는 자식 클래스인 DaoAuthenticationProvider에 정의돼있습니다.

additionalAuthenticationChecks

코드를 확인해보면 authentication에 인증 정보가 있는지 확인하고

passwordEncoder.matches()를 통해

사용자가 입력한 비밀번호를 암호화한것과 db에 존재하는 암호화된 비밀번호가 같은지 비교합니다.

만약 일치한다면 additionalAuthenticationChecks메소드는 아무것도 리턴하지않고 종료되고

authenticate()메소드는 createSuccessAuthentication(principalToReturn, authentication, user)를 리턴합니다.

일치하지않는다면 BadCredentialsException예외를 던지게됩니다.

passwordEncoderSecurityFilterChain 빈을 정의하는 클래스에
passwordEncoder를 리턴하는 빈을 정의하면 됩니다.
여러 passwordEncoder가 있으니 원하는 알고리즘을 고르시면 됩니다.

모든 코드에서 예외가 발생하지않고 정상적으로 진행된다면

맨처음의 doFilter에서 successfulAuthentication()이 호출되고

기본적인 스프링시큐리티의 인증이 완료됩니다.

마치며

사실 캡쳐 이미지와 설명때문에 길어지긴했지만..

정말 기본적인 인증과정만 담았습니다!

스프링시큐리티에는 더 깊고 많은 기능들이 있지만

이 내용으로 첫 단추를 끼우고 시작한다면

조금이나마 수월하게 스프링 시큐리티를 익혀나갈 수 있지 않을까 싶습니다!

혹시나 잘못되거나 부족한 부분이 있다면 댓글로 알려주세요!

감사합니다.