ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring security - 3. Security설정한 거 끝까지 파보기
    Spring Security/security 2023. 11. 8. 22:00
    728x90

    지난번 글에서 UserDetailsService를 사용하여 security로그인 기능을 구현하였다.

     

    내가 작성한 것은 userDetailsService에 하나 있는 loadUserByUsername 메소드를 override한거고,

    그 내용은 db에서 조회 후 security가 원하는 UserDetails 라는 객체에 넣어줬을 뿐인데,

    그 후에 이 놈이 뭘 했길래 session에 그 정보를 저장하는지 궁금했고,

     

    1장에 있는 그 그림대로 움직이는게 어떤건지 궁금해서 userDetailsService를 시작으로 메소드가 끝날때 까지 파보기로 하였다.


    우선 내가 담아보낸 User와 UserDetails를 먼저 살펴봤다

    내가 사용했던 User 객체

    • Security에 기본으로 작성되어있던 User class, 이 놈이 UserDetails 를 구현했기 때문에 loadUserByUsername 메소드에서 이놈을 return 해 줄 수 있었다.
    • 내부를 살펴보면 User의 필드 값들이 설정 되어있고, 비교적 간단한 생성자 한 개와, 모든 필드값들을 전부 넣어줘야 하는 조금 더 복잡한 생성자 이렇게 2개의 생성자를 가지고 있었다.
    • 그 아래에는 각 필드의 getter들이 존재했고, 이 getter들은 UserDetails에서 override 되었다.
    • equals와 hashCode를 override해서 구현했는데, 객체의 equals는 객체의 주소값, hashCode는 객체의 hashCode를 가져오는 메소드다. 보통 override를 하지 않지만, 위처럼 사용자 관련, entity관련 해서는 override를 한다.
      • 자세한 내용은 JPA때 적기로 하고, 간단하게 적아본다면 객체가 여러개 생성돼있을 때, override를 하지 않은 경우엔 객체들의 주소값이 같지않아 서로 다른 객체이지만, User와 같이 특별한 경우 주소가 다르더라도 unique한 username 값이 똑같다면 같은 객체로 볼 수 있기 때문에 Entity와 User 같이 전체가 아닌 일부가 같으면 같은 객체라고 인식하기 위해 equals와 hashCode를 override하여 수정한다. (String도 들어가보면 override 되어있음)
    • 그 밑에는 UserBuilder로 생성자 대신 Builder 로 User를 만들수 있게 작성해놨다.

    sassion으로 어떻게 접근하는지 아직 모르겠지만 UserDetails의 추상메소드를 사용하여 sassion에 값을 넣는 것 같다.


    UserDetailsService를 디버깅을 통해 흐름을 따라가보자

    loadUserByUsername에 마킹
    f8!!
    DaoAuthenticationProvider

    • 계속 f8을 누르다보니 DaoAuthenticationProvider의 한 메소드로 이동되었다.

    • DaoAuthenticationProvider의 필드값 userDetailsService는 내가 구현헀던 userDeetailsService interface이고, 내가 구현한 loanUserByUsername을 호출하여 위에서 본 User 객체를 통해 자기가 원하는 UserDetails 값을 가져왔다.
    • 나는 값을 잘 가져왔기 때문에 if와 catch로 빠지지 않고 바로 return 되었다.

    AbstractUserDetailsAuthenticationProvider

    • DaoAuthenticationProvider는 또 자기를 호출 헀던 AbstractUserDetailsAuthenticationProvider를 호출했었다.
    • 이녀석이 호출 했던 곳을 보니 맨 처음에 if로 user is null 이라는 조건문 후 호출된 것을 알 수 있다.
    • 그 위헤 맨처음 user를 생성한 곳을 보니 userCache 라느 곳에서 내가 우선 있는지 확인하는 것 같은데, 메소드 명만 봐도 케시에서 username으로 된 값이 있는지 확인하는 것 같다. (우선 나는 session에 어떻게 저장하는가? 가 궁금하니 나중에 확인하기로 하고...)

    AbstractUserDetailsAuthenticationProvider

    • 누군진 모르겠지만 preAuthenticationChecks란 곳에서 user를 체크하는 것 같다.

    userDeteailsChecker

    • 간단한 interface가 있었고, 저걸 구현한 3가지 클래스 AccountStatusUserDetailsChecker, DefaulstPostAuthenticationChecks, DefaultPreAuthenticationChecks 이렇게 있는데, 앞서 이름이 preAuthenticationChecks 였으니 3번째인 DefaultPreAuthenticationChecks 이길 기대하고 마킹을 했다.

    AbstractUserDetailsAuthenticationProvider

    • 예스! 맞았다.
    • 이 놈은 아까 위에서 봤던 AbstractUserDetailsAuthenticationProvider 내부에 있는 클래스였다.
    • 메소드는 별거 없었고, User에 있던 필드값을 가지고 검증을 처리하는 역할만 헀다.
    • 이름에 pre가 들어가니 ~전 인것 같고 authentication이 붙었으니 인증전에 check를 하는 객체인 것 같다.

    AbstractUserDetailsAuthenticationProvider

    • 다시 돌아와서 additionalAuthenticationChecks를 통해 먼가를 진행 할 것 같다.
    • 갑자기 UsernamePasswordAuthenticationToken이 나타났는데, 우선 메소드를 확인해봐야겠다.

    DaoAuthenticationProvider

    • 갑자기 나타는 UsernamePasswordAuthenticationToken(이하 authentication)이 쓰이니까 뭔가 당황했다.
    • 첫번째 if문으로 에러가 발생 할 수 있는 코드가 있었지만, 나는 정상적으로 로그인이 됐으니까 if문을 빗겨나간 것 같다.
    • credentials가 null이 아니니까 갑자기 그 값을 presentedPassword라고 정했다. 그리고 그 값을 passwordEncoder를 통해 UserDetails에 있는 password와 비교한다.
    • 생각해보니 아까 내가 구현한 userService에서는 password없이 userId로만 값을 가져와서 비밀번호 체크도 안하고 바로 가져갔었다. 아마 loginProcessingUrl을 통해 userId와 password가 넘어오면 두개를 따로 관리해서 userId로만 우선 값을 가져오고 password는 더 안전?하게 authentication의 credientials에 저장해 Security내부에서만 사용하는 것 같다.

    AbstractUserDetailsAuthenticationProvider

    • 위 인증도 통과해서 이번엔 DefaulstPostAuthenticationChecks를 통해 인증 후 뭔가를 체크하는 것 같다.

    AbstractUserDetailsAuthenticationProvider

    • if문 한개로 끝나는 메소드인데 이름에 isCredentialsNonExpired인거 보니 해당 인증 정보가 유효시간? 을 넘기지 않았냐 물어보는 것 같다. 앞에 !가 붙어서 넘겼으면 안의 에러를 발생시키는 것 같다.
    • 나의 User 정보는 여기도 뛰어 넘었다.

    AbstractUserDetailsAuthenticationProvider

    • cacheWasUsed가 갑자기 false라길래 의심이든 곳이 있어 올라가 봤다. 맨 처음 security내부 코드로 들어왔을 때 if문이 user is null 이였고, 그 위에가 cache에서 username을 가지고 조회하고 있었는데, 조회 전에 cacheWasUsed를 true로 설정해놓고, 나의 User처럼 cache에 없을 때에는 다시 false로 설정하여 해당 메소드를 타게 했다.
    • 메소드 이름을 보니 다음에 로그인 할 때에는 cache에서 값을 찾을 수 있게 putUserInCache를 하는 것 같다.

    userCache

    • 인터페이스는 3개의 추상 메소드를 가지고 있었고 내가 확인 할 putUserInCache는 NullUserCache와 SpringCacheBasedUserCache 두개의 클래스에서 구현하고 있었는데, 아직 등록이 안되었으니 null인가? 아니면 User라는 객체가 null이 아니니까 SpringCacheBasedUserCache인가? 모르겠어서 두 곳 모두 체크를 했고, NullUserCache인 것을 확인하였다.

    NullUserCache

    • ? 아무런 동작을 안하는데?

    AbstractUserDetailsAuthenticationProvider

    • UserDetails를 Object 에 담고, createSuccessAuthentication메소드로 이동하였다.

    AbstractUserDetailsAuthenticationProvider

    • 해당 메소드는 아까 봤던 UsernamePasswordAuthenticationToken을 만들어서 return 하는 메소드 같다.
    • principal은 나의 UserDetails이고 authentication은 로그인 요청시 만들어진 객체, 마지막 User는 Object에 담지않은 user 이렇게 3개다.  authenticated 메소드에서 authentication.getCredentials를 사용하는거 보니 비밀번호 정보를 넣는 것 같다.

    UsernamePasswordAuthenticationToken

    • 해당 생성자로 연결되어있고 AbstractAuthenticationToken을 상속받았는데, 인증정보들은 AbstractAuthenticationToken을 통해 생성하는 것 같다.
    • 생성만 하는 것 같아 AbstractAuthenticationToken은 들어가지 않았다.

    ProviderManager

    • 타고타고 나오니 어느 한 클래스의 메소드중 한 부분으로 이동 되었다 (이렇게 따라가니까 뭔가 점점 세상밖으로 나가는 느낌이 든다)
    • 아까 갑자기 등장했던 UsernamePasswordAuthenticationToken 객체를 여기서 넣어줬던거였다. 근데 이놈도 자기도 전달받아 넣어 준 것 같아, 이놈 위에도 뭔가 있는 느낌이 든다.
    • 호출 됐었던 메소드에서 null이 아닌 UsernamePasswordAuthenticationToken 을 던져줬기 때문에 if문에서 걸리게 되었다.
    • 해당 메소드는 갑자기 등장헀던 UsernamePasswordAuthenticationToken 과 호출 됐었던 UsernamePasswordAuthenticationToken 를 넣어 동작하는 것 같은데 이름이 copy인거보니 어디서 어디로 복사하는 것 같다.

    ProviderManager

    • 아규먼트 이름이 바꼈는데, source는 갑자기 나타났었던 UsernamePasswordAuthenticationToken 이고, dest는 호출돼서 만들어진  UsernamePasswordAuthenticationToken  다
    • dest가 null이 아니여서 해당 메소드는 타지 않았다.

    ProviderManager

    • 갑자기 나타난 break. 확인해보니 호출 한 메소드는 for loop를 돌고 있었고, result 값이 null이 아닐 때 까지 돈 것 같았다. 해당 for loop는 provider list를 가지고 for loop를 돈 것 같은데, provider가 공급자인건 알지만 위의 Provider가 붙은 객체를 보니까 인증을 공급? 해주는 것들이 여러가지가 있는데, 그중에 UserDetailsService가 호출 된 건 AbstractUserDetailsAuthenticationProvider 가 불러서 인 것 같다.

    providerManager

    • (result는 호출돼어서 만들어진 UsernamePasswordAuthenticationToken 이다 네이밍이 저래서 까먹기 편한 것 같다)
    • 호출된 메소드에서 null을 보내지 않았기 때문에 내부 로직이 실행된다.

    providerManager

    • 갑자기 나타난 eraseCredentialsAfterAuthentication. 위에 올라가보니 처음부터 true라 설정되어 있었고, 밑에 하나의 메소드를 통해 boolean 값을 전달받아 값이 변경되도록 설정되어 있었다. 목표는 session 저장이니 나중에 살펴보기로 하고.
    • 아까 만들어진 UsernamePasswordAuthenticationToken 가 이번엔 CredentialsContainer와 같다고 한다. ( UsernamePasswordAuthenticationToken 를 확인 못해봤지만 뭔가 상속받고 그런 것 같다)
      • UsernamePasswordAuthenticationToken은 AbstractAuthenticationToken을 상속받고있고, AbstractAuthenticationToken은 Authentication과 CredentialsContainer를 구현하고 있다.

    providerManager

    • 메소드명을 보니 Credentials를 지우는 것 같다.
    • 따라가 보니 UsernamePasswordAuthenticationToken 에 override한 eraseCredentials를 호출하고 있었고, 그 메소드는 AbstractAuthenticationToken에서 구현한 eraseCredentials를 호출 한 뒤에 자기 자신의 credentials을 null로 설정하였다. (credentials는 아까 toString으로 해서 presentedPassword로 바꿨었는데 그 값은 로그인시 입력한 비밀번호)
    • AbstractAuthenticationToken 에서 타고타고 가서 User에서 구현된 eraseCredentials를 호출 했다. 해당 메소드는 password = null 로 설정했다.
    • 여기서 result에 남은 것을 확인해보면 principal의 username과 details의 sessionId, macAddress 이렇게 3개가 남아있다.
    • 헐. 언제 details에 sessionId가 들어 갈 수 있었지?

    providerManager

    • 갑자기 등장한 parentResult. 이놈은 위에서부터 null로 설정되어있었고,
    • 위 if문 전에 등장한 result is null일때 무슨 작업을 하고 result를 해당 parentResult에 다시 넣는 로직이 있었지만 타지 않았다.
    • 해당 eventPublisher interface는 2개의 클래스에서 구현했는데, 두곳다 마킹을 하고 넘어갔다.

    DefaultAuthenticationEventPublisher

    • 이미 생성되어있던 applicationEventPublisher라는 놈이 있어서 if문 내부로직이 동작했다.
    • AuthenticationSuccessEvent라는 객체로 만들어 메소드의 파라메터로 넣는데, 해당 생성자를 확인해보니, 상속받은 부모로 계속 타고 올라가다 ApplicationEvent라는 곳에서 Object로 필드값에 추가되었고, 현재 시간을 timeStamp에 담아 다른 필드값으로 저장하여 생성하였다.

    ApplicationEventPubliser

    • publishEvent는 default로 설정되어있었고 Object로 cast하여 추상 메소드로 변경되 있었다.
    • 총 3곳에서 구현되어있었는데, 두곳은 아무런 로직이 없고 한 곳만 로직이 있었다. ( AbstractApplicationContext )

    AbstractApplicationContext

    • 해당 로직이 굉장히 길어서 다시 집중해야겠다.
    • User정보는 윗 단계에서 더이상 User정보가 아닌 ApplicationEvent로 바뀌어서 해당 if문이 true고, 로직을 통해 Object로 감싸졌던 event를 다시 ApplicationEvent로 넣었다.
    • 해당 메소드에서 eventType값이 처음부터 null로 들어와서 밑의 로직을 타게 되었다. 위에 주석을 해석해보니, 이벤트 유형을 하나만 결정한다는 건데, 계속해서 ApplicationEvent였기 때문에 이걸로 결정될 것 같다. (로직이 너무 어려워)

    ResolvableType

    • 먼가 ApplicationEvent는 ResolvableTypeProvider와 연관이 없는 것 같아, if로직을 타지 않게 되었다.
    • 근데 밑에 forClass를 통해 ResolvableType으로 강제로 넣었다.

    AbstractApplicationContext

    • 아까 null이였던 typeHint를 위에서 만든 ApplicationEvent로 넣었다.

    이 뒤로 먼가 로직이 있는데, 점점 내용 이해도 안되고 해서 따라가는 것이 아니라 쭉 훑어 보았다... 나는 Session에 언제 넣는지 궁금했는데, 이렇게 많이 타고타고 할 줄이야..

     

    AbstractAuthenticationProcessingFilter

    • f8로 넘기다보니 익숙한 이름의 메소드를 발견하였다.
    • Spring security 1에서 적었던 Spring security는 여러 filter들을 chain으로 연결하여 인증했다는 것인데, 그 filter들의 메소드가 doFilter로 기억하고 있다.

    FilterChainProxy

    • 갑자기 1장에서 봤던 익숙한 이름이 보였다. FilterChainProxy. 
    • 지금까지 내가 securityConfiguration에 적었고, 설정하지 못해 default로 남아있던 filter들이 순서대로 돌고 있었던 것이였다.

    logoutFilter

    • f8을 눌렀더니 logoutFilter로 옮겨졌다. 왜지?
    • 다시 누르니 FilterChainProxy로 왔고, 또 f8을 누르니 HeaderWriterFilter로 이동 되었다.
    • 다음 SecurityContextHolderFilter
    • 다음 WebAsyncManagerIntegrationFilter
    • 다음 DisableUrlEncoderFilter

    DelegatingFilterProxy

    • 위 filter들을 거치고 나니까 1장에서 본 DelegatingFilterProxy가 나타났다.
    • DelegatingFilterProxy는 Servlet container에서 servlet으로 가기전에 filter들을 거치는데, Spring security 설정을 하면 메인 filter을 이 DelegatingFilterProxy라는 녀석이 대체해서 설정한 security filter들을 돌린다고 알게되었었다.

    ApplicationFilterChain

    • 상위? 녀석으로 ApplicationFilterChain이 나타났는데, 여기 메소드의 ServletRequest에서 client에서 보낸 로그인정보가 담겨있지 않나? 라는 생각이 들었다.
    • 저런 로직을 통해 DelegatingFilterProxy를 호출하여 request정보를 전달하여 FilterChainProxy를 통해 서버가 생성되기 전에 설정한 security설정을 바탕으로 로그인 과정이 일어나는구나 라고 짐작 할 수 있었다.

    위로써 끝날줄알았는데, 이 뒤로도 범잡을 수 없을만큼 많은 로직을 타다가 메소드가 끝났다.

     

    시작과 끝, Session을 저장하는 위치 이런거에 대해 궁금했는데, 체력적으로 많이 부족했다..

    728x90
    반응형
    LIST

    댓글

Designed by Tistory.