woowacourse-teams / 2023-festa-go

🎪 페스타고, 대학 축제를 더욱 즐겁게!
71 stars 8 forks source link

[BE] refactor: JWT에 권한에 따른 클레임을 담기 위해 인증 기능 개편 (#982) #985

Closed seokjin8678 closed 4 months ago

seokjin8678 commented 4 months ago

📌 관련 이슈

✨ PR 세부 내용

이슈에서 적은 내용대로 JWT의 페이로드의 권한에 따른 클레임을 담기 위해 기존 인증 기능을 새롭게 리팩터링 했습니다.

우선 기존 인증 기능의 프로세스를 정리하자면 다음과 같습니다.

  1. 인증이 필요한 URL 경로 또는 핸들러 메서드에 인증과 관련된 어노테이션이 붙어 있는 경우 AuthInterceptor 접근
  2. 접근된 AuthInterceptor에 따라 정의된 HttpRequestTokenExtractor에 따라 토큰 추출 2.1 Request Header, Cookie에서 토큰을 추출하는데, 둘 중 하나만 허용, 만약 변경하려면 코드의 수정 필요 -> 문제1
  3. 토큰을 파싱하여, AuthPayload 객체 반환 -> 인증
  4. AuthPayload의 Role이 해당 인터셉터가 허용하는 권한과 맞는지 검증 -> 인가
  5. AuthenticateContext에 인가된 사용자의 식별자와 권한 부여
  6. 컨트롤러의 핸들러 메서드의 파리미터 중 @Member와 같은 에너테이션이 붙어있는 Long 타입의 파리미터에 RoleArgumentResolver를 통해 AuthenticateContext에서 부여된 식별자를 할당 6.1 이슈에서 지적한 내용이지만, 식별자로만 받기 때문에 해당 권한의 사용자가 어떠한 권한을 가졌는지 확인하려면 DB를 거쳐야 함 -> 문제2

문제1은 간단히 CompositeHttpRequestTokenExtractor를 사용하는 것으로 해결되었습니다만, 문제2가 이 PR의 핵심입니다.

문제2를 해결하려면 AuthenticateContext에 식별자와 Role 필드뿐 아닌, 해당 권한을 가진 사용자가 가진 값들이 필요합니다.

하지만 해당 값들은 권한마다 다를 수 있기에 모든 값을 AuthenticateContext에 필드로 둘 수 없습니다.

따라서 Authentication이라는 인터페이스를 정의하여 해당 인터페이스를 의존하게 한 뒤, Authentication를 받는 ArgumentResolver를 정의하여 사용해야 합니다.

그러면 권한마다 별도의 Authentication 구현체가 생기는데, 토큰의 권한을 구분하여 Authentication 구현체를 생성하는 기능이 필요합니다.

따라서 해당 권한을 구현하기 위해 AuthenticationTokenExtractor을 설계하였고, AuthenticationTokenExtractor에서 토큰의 권한을 구분하여 Authentication 구현체를 반환합니다.

이때 AuthenticationTokenExtractor 구현체를 보면 토큰을 파싱하여 바로 Authentication 객체를 반환하지 않고, 그저 AuthenticationTokenExtractor 구현체를 의존한 뒤, 이를 위임하여 사용하는 것을 볼 수 있습니다.

이렇게 구현한 이유는 문제1을 해결한 것과 비슷한데, 토큰을 추출하는 과정에서 권한에 따라 다시 여러 번 토큰을 파싱하지 않게 하기 위함입니다.

구현한 인증 기능을 사용하려면 컨트롤러의 핸들러 메서드에 @Authorization 어노테이션을 사용한 뒤, 해당 어노테이션의 필드에 Role을 추가하면 되는데, 이게 상당히 번잡합니다. 😂 (기존 @MemberAuth 어노테이션이 붙은 컨트롤러도 모두 수정해야 하는 문제점도 발생)

하지만 HandlerMethod.getMethodAnnotation() 메서드에는 숨겨진 하나의 비밀이 있는데, 해당 메서드에 다음과 같은 주석이 적혀있습니다.

Also supports merged composed annotations with attribute overrides as of Spring Framework 4.2.2.

이는 타겟으로 한 어노테이션이 아니더라도, 해당 핸들러 메서드의 어노테이션에 타겟으로 하는 어노테이션이 있으면, 해당 어노테이션을 사용할 수 있습니다.

그래서 MemberAuthV1Controller의 핸들러 메서드에는 Authorization 어노테이션이 붙어있지 않은데, 인증과 관련된 테스트가 모두 통과하는 것을 볼 수 있습니다.

여러 디자인 패턴이 사용되었고, 인터페이스와 구현체가 많기에 자세한 설명은 코드에 리뷰로 남기겠습니다!

github-actions[bot] commented 4 months ago

Test Results

244 files  244 suites   29s :stopwatch: 797 tests 797 :white_check_mark: 0 :zzz: 0 :x: 815 runs  815 :white_check_mark: 0 :zzz: 0 :x:

Results for commit ebd3ce7b.

:recycle: This comment has been updated with latest results.