Security(백지 정리)
Security란?
- Spring에서 제공하는 인증/인가 기능. Session, JWT & 암호화 방식을 대체할 수 있다.
Dr.G의 가이드
질문 1:
"Spring Security가 없으면 로그인이나 권한 처리를 어떻게 해야 할까?"
답변:
"JWT나 Session을 사용하는 경우 1) Session인 경우 로그인 시 Session을 부여하는데, 이 세션(SessionAttribute)에 로그인한 유저의 정보를 담아줌. 권한 처리의 경우 이 세션에 담긴 정보와 요청에 필요한 정보가 일치하는지를 확인해서 처리. 이때 Filter를 사용해 인증/인가가 필요한 API 요청을 정의할 수 있음. 2) JWT인 경우에도 마찬가지로, 토큰을 부여하는데 토큰을 해독하면 로그인한 유저의 정보를 확인할 수 있도록 함(Claim에 저장) 마찬가지로 필터를 활용해 인증/인가가 필요한 API 요청을 정의할 수 있음"
"Spring Security를 사용하는 것과 JWT / Session을 사용하는 것 사이의 장단점은 잘 모르겠다!"Dr.G의 피드백
- 아! Spring Security와 JWT/Session이 분리된 개념이 아니라, Spring Security는 둘을 편리하게 구현할 수 있도록 해주는 프레임워크구나! 이 프레임워크에 Session, JWT 등의 인증 수단을 연동해 사용할 수 있는 거고!
- 지금의 방식도 좋지만, 좀 더 구조화된 사고 연습을 하면 좋다고 한다!
Q1. Session과 JWT는 어떤 기준에서 선택해야 할까?
- A1. 앱서비스에는 Session을 사용하기 어려워 JWT를 사용한다고 한 것 같은데, 왜 그런지 생각해보자. Session은 서버에서 데이터를 가지고 있고, JWT는 클라이언트에서 데이터를 가지고 있다고 배운 것 같다. 물론 JWT도 서버에서 인증키를 갖고 있지만, 인증키가 같은지 여부만 검증하고 같으면 서버(DB) 접근 권한을 열어주므로 서버에서 갖고 있는 데이터 자체가 가볍다. 반면 Session안에는 필요한 유저 정보가 담겨 있어 서버가 관리하는 데이터가 무겁다. 또한 Session은 유저가 인터페이스를 종료하는 경우 사라지지만, 토큰은 일정 시간 유효해 일정 시간 이후에 접속해도 로그인이 유지된 상태로 활용할 수 있다. 앱 서비스는 일정 시간 접속을 하지 않다가 앱에 다시 접속하거나 하는 경우가 잦은데 이때마다 로그인을 하면 번거롭기 때문에 Session을 사용하기에 부적합한 것이 아닐까. 그리고 은행 서비스와 같이 보안이 중요한 경우에는 Session을 사용해 브라우저 종료 후 다시 사용 시 새롭게 로그인을 하도록 하는 것이 맞을 것 같은데, Session은 정보 보호가 되지 않으니 또 이게 맞나 싶기도 하다.
Dr. G 피드백
- Sessin도 만료 시간을 설정한다!
- Session은 서버가 모든 사용자 세션 데이터를 메모리에 저장 -> 사용자 수 증가에 따라 서버 자원 부담 커진다. <-> JWT는 인증 정보를 클라이언트가 토큰에 담아 보관해서, 서버는 "토큰의 유효성만 검증"하면 된다 -> Stateless 구조 (서버 부하 적음)
- 보안은 둘 다 위험이 있다.
- 최종 정리
Q2. Filter로 인증/인가를 직접 구현하는 경우와 Spring Security를 사용하는 경우, 유지보수나 확장성 면에서 어떤 차이가 있을까?
- A2. 처음 이 질문을 받았을 때, 이건 정말 모르겠다는 생각이 들었다. 프로젝트를 했던 경험을 다시 생각해보자. Session의 경우 LoginFilter라는 클래스를 만들었고, JWT의 경우 JWT를 사용하기 위한 JwtUtil과 JWT를 필터로 활용하기 위한 JwtFilter 클래스를 만들었다. 이 필터에서 doFilter라는 메서드를 정의했던 것 같다. httpServletRequest(reqeust)에서 1) Session인 경우 request.getSession()을 하고, 이 세션에서 SessionAttribute를 받아와 인증을 했고, whitelist에 해당하는 API URL을 제외하고는 모두 인증을 진행하도록 했다. 이 filter는 여러 개를 연결할 수 있어서, 자동으로 request와 reponse가 연결되어 필터 처리를 하고, 하나라도 걸리면 Exception을 반환했던 것 같다. 근데 구체적은 프로세스는 기억이 나지 않는다. 2) JWT인 경우 request에서 token을 받아와 token key를 검증하고 이 토큰에서 Claim을 꺼내와 claim 내의 정보를 조회하는 방식을 사용했다. JWT에서는 @Auth라는 방식도 사용했던 것 같은데 맞는지 가물가물하다. AuthArgumentResolver라는 개념도 같이 들어갔던 것 같은데.. 로직은 Session과 유사하나 이런 설정들이 들어갔던 것 같다. @Auth AuthUser 이렇게 해서 인증이 필요한 API에는 request에 이 파라미터를 넣어줬던 것 같다. 아 FilterConfig도 설정했던 것 같다.
- 유지보수나 확장성 면에서는 어떤 차이가 있을까? SpringSecurity는 FilterConfig가 아닌 SecurityConfig를 사용했던 것 같고, CustomPrincipal, 이런 클래스들을 설정해줬던 것 같다. 이게 유지보수나 확장성 면에서 어떤 차이가 있는지 잘 모르겠다. 도움이 필요하다!
Dr.G 피드백
- 질문에 대한 답변이 부족하다! ㅋㅋㅋ 인정. 의식의 흐름으로 Filter 방식과 SpringSecurity를 떠올리다 보니 아는 것만 정리했는데, 아는 것 중에 유지보수나 확장성 관련한 특징은 없었다..
- 유지보수나 확장성은, 설정과 관련이 있다! 오! 근데 이거 보니까 Spring Security가 엄청 편하고 좋아 보이는데? 조그만 칼 쓰다가 만능 칼을 쓰는 느낌이다.
- Dr.G의 최종 정리
- 이런 질문으로 사고를 넓히라고 한다.
Q3. Spring Security는 내부적으로 어떻게 인증 절차를 처리할까? (ex: UsernamePasswordAuthenticationFilter는 언제 실행될까?)
- A3. 이건 정말 모르겠다. 코드를 작성하면서도 설정 값 각각이 어떤 역할을 하는지 알기 어려웠다. 도움이 필요하다!
Dr.G 피드백
- 먼저 질문을 재정의하라고 한다. "어떤 객체들이 어떤 순서로 동작하면서 로그인을 처리하는지"를 정리해보자
- 핵심 구조 흐름은 필터 체인 구조다. HTTP 요청 > SecurityFilterChain > UsernamePasswordAuthenticationFilter (or JwtAuthenticationFilter 등) > AuthenticationManager > AuthenticationProvider > UserDetailsService > UserDetails (DB에서 유저 정보 조회) > 인증 성공 (SecurityContextHolder에 저장) > 인가(Authorization) 단계로 넘어감
- 그럼, 각 요소가 뭔지 살펴보자. 대략 이해는 가는데 100%는 아닌 느낌s 와닿지 않아서 그런가.
- 인증 시 실제 순서로 보면 아래와 같다. 로그인 시 UsernamePasswordAuthenticationFilter가 ID/PW를 추출하고, 이 정보로 UsernamePasswordAuthenticationFilterToken을 생성하고, AuthenticationManager가 AuthenticationProvider를 호출해 UserDetailsService에서 DB를 조회하고, 비밀번호 일치 시 인증에 성공하는데 인증된 사용자 정보(Authentication)를 SecurityContextHolder에 저장한다. 이후의 요청들은 이 인증 정보로 처리한다고! 아! SecurityContextHolder에 인증된 사용자 정보가 담겨 있구나! 그래서 튜터님이 SecurityContextHolder가 중요하다고 하셨구나.
- 아래와 같이 정리해주었다
- 아래와 같은 사고 확장을 위한 질문도 남겨주었다..
사고 확장을 위한 질문 & 답변 정리
Q1. 만약 서비스에 SSO(Single Sign-On)를 붙여야 한다면, 직접 구현할 필터 구조로 가능할까?
A1. SSO가 뭔지 모르겠다.Dr.G
Q2. 새로운 정책(예: 관리자만 특정 API 접근 가능)을 추가하려면 어느 쪽이 더 적은 코드로 가능할까? (Filter vs Spring Security)
A2. Spring Security. 이유는 잘 모르겠다.Dr.G
Q3. 보안 감사를 대비해 인증 흐름을 설명해야 한다면, 어느 쪽이 더 신뢰를 줄 수 있을까?
A3. Spring Security. 표준화된 프레임이 있고 보안 기능을 기본적으로 제공하므로 안정성이 높아 더 신뢰를 줄 수 있을 것 같다.Dr.G
Q4. JWT를 쓸 때는 UsernamePasswordAuthenticationFilter 대신 어떤 필터를 만들어야 할까?
A4. JwtAuthenticationFilterDr.G
Q5. 로그인 외에 토큰 기반 인증은 어떤 시점에 필터가 실행될까?
A5. whiteList 외에 있는 API 요청 혹은 API에 @Authentication(?)을 요청한 APIDr.G
Q6. 만약 /login이 아닌 경로를 로그인 경로로 바꾸고 싶다면 어떻게 설정할까?
A6. 이건 잘 모르겠다.Dr.G
Spring Security의 장/단점
- 장점
- 위 과정을 거치면서 배운 점들을 정리해보면, 초기 세팅만 하면 이후 변경 사항에 유연하게 대처할 수 있어 유지 보수와 확장성에 강점이 있으며, CSRF 등의 보안 라이브러리도 내장하고 있어 안정적이다. Filter를 직접 구현하느냐, Spring Security를 사용하느냐의 갈림길에서 Filter를 직접 구현하는 방식은 초기에 쉽게 느껴질 수 있으나, Spring Security를 사용하는 것이 현업에서의 BP다!
- 단점
- 처음 Spring을 입문하고 배우는 입장에서 설정해야할 요소들이 많고, 각각을 어떻게 설정하고 활용해야하는지 숙지하는 과정이 필요한 것 같다. 다만, 제대로 숙지하기만 하면 활용도가 높은 라이브러리(?) 프레임워크(?)다!
Dr.G
- 장점 코칭. Spring Security는 보안 라이브러리가 아니라 보안 기능이네!
- 현업에서는 대부분 Spring Security를 사용한다고 하니 제대로 익혀둘 필요가 있겠다. 한번 더 복습 필요할 듯. Spring Security의 구성 요소와 동작 순서, 역할을 숙지하자!
Security 설정 (JWT 활용 시)
- SecurityConfig
- 직접 필터를 구현할 때 WebConfig를 구현했던 것과 비슷한 방식으로 동작하는 것이 아닐까? 구조와 역할은 잘 기억이 나지 않는다.
- build.gradle
- Security를 사용하기 위한 dependency 설정이 필요함
- JwtUtil / JwtFilter 수정
- JwtUtil은 Jwt를 어떻게 사용하는가에 대한 클래스. Key 설정과 Claim에 어떤 값을 넣어줄지, Token은 어떻게 검증할지에 대한 메서드들을 정의한다.
- JwtFilter는 Jwt를 활용해 필터를 어떻게 구성할 것인지에 대한 부분. 토큰을 검증하는 역할만 해주면 되고, OncePer 뭐시기 클래스를 상속받았던 것 같다. 위에서 정리해온 내용에 따르면, UsernamePasswordAuthenticationFilter 이전에 동작해 토큰의 유효성을 검증하고, 유효성이 검증되면 토큰을 다음 필터과정으로 넘겨주는 역할만 하는 것이 아닐까 하는 생각이 든다.
- @Auth Annotation 수정
- AuthArgumentResolver 대신 SecurityConfig를 사용하면서 AuthUser를 사용했던 Annotation들을 모두 다른 것으로 수정해주었는데, 그러면서 받아오는 필드를 CustomPrincipal 관련된 것으로 받았던 것 같다. 정확히는 기억이 나지 않는다.
- CustomPrincipal
- 이것도 어떤 클래스(인터페이스)를 상속받았던 것 같다. UserDetails와 관련이 있었던 것 같기도 하다.
- SecurityResponseDto
- UserDetails(SecurityContextHolder)에 담을 필드만 따로 정의하기 위해 설정했던 것 같다.
Dr.G
- 사고 정리법 제안
- WebConfig와 SecurityConfig는 목적이 다르다! 와 근데 안에 있는 질문에 하나도 답을 못 하겠다. 하나씩 해보자
Q1. SecurityConfig는 어떤 시점에서, 어떤 필터를 등록하는가?
A. ServletRequestController 동작 이전에 UsernamePasswordAuthenticationFilter를 등록한다.
Q2. SecurityFilterChain을 Bean으로 등록할 때 어떤 Filter들이 순차적으로 적용되는가?
A. UsernamePasswordAuthenticationFilter 밖에 생각나지 않는다.
Q3. SecurityFilterChain 내부에 JWT 필터를 넣을 때 addFilterBefore()를 사용하는 이유는?
A. UsernamePasswordAuthenticationFilter 동작 이전에 정상 요청을 하는 유저인지 JWT 필터가 먼저 토큰을 검증해줘야 하므로
Q4. Spring Security에서 Filter 순서는 왜 중요한가?
A. Filter를 거치면서 Security 검증이 이루어지기 때문이다. UsernamePassword가 일치하는지, DB에 있는 정보와 일치하는지 등 순서대로 Filtering이 필요하기 때문이다. (이 부분도 구체적으로 모르니 추상적인 답변 밖에 안 나온다)
- Build.gradle도 구체적으로 알고 넘어가자!
Q1. JWT 관련 의존성 중 jjwt와 nimbus-jose-jwt는 어떻게 다를까?
A. 잘 모르겠다. 둘 다 처음 들어보는 것 같다.
- jjwt
- nimbus-jose-jwt
Q2. 각 라이브러리의 역할과 차이점은?
A. 이것도 답변이 어렵다.
- 역할과 차이점
- 사고력 넓히기 ... 이것도 나중에 해보자! jjwt가 쉬운 버전이고, nimbus-jose-jwt가 고급 버전인 것 같다. jjwt가 일반 웹 서비스 인증용으로 추천하는 걸 보면, 앱 서비스나 고급 보안이 필요한 경우엔 nimbus-jose-jwt를 사용하는게 좋겠다!
- 3. JwtUtil / JwtFilter
Q1. 이 유틸 클래스는 어디서 호출되고, 어떤 책임(Single Responsibility)를 가져야 할까?
A. SecurityFilterChain에서 호출되고 유저가 갖고 있는 인증 토큰이 유효한지 검증하는 책임을 가져야 한다.
Q2. Claim에 어떤 정보를 담을지 어떻게 결정했는가?
A. 직접 필터를 구현할 때는 비즈니스 로직에서 필요한 정보를 담았는데, Spring Security 기술을 사용할 때는 유효한 유저이고 인증 정보가 일치하는 경우 DB에서 해당하는 유저의 UserDetails에 필요한 정보를 담을 수 있으므로 유저의 역할 정도만 담아주면 충분했던 것 같다.
Q3. Claim에 email vs userId vs role 중 무엇을 넣는 게 좋을까?
A. role. Claim에는 role만 넣어 유효한 요청을 하는 유저인지 여부를 우선 검증하면 추후 API 요청 시마다 적절한 요청인지 빠르게 검증할 수 있다.
Q4. JwtFilter는 왜 OncePerRequestFilter를 상속받았을까?
A. 잘 모르겠다. 이름에서 유추해보면, 요청 당 한 번만 수행하는 필터라고 하는데, 최초에만 토큰을 검증하고, 이후 요청들에는 JwtFilter로 검증하지 않고 securityContextHolder에 담긴 정보를 활용하는 건가? 싶다. 근데 생각해보면 서비스 이용하다가 중간에 토큰이 유효하지 않습니다라는 에러 메시지가 나오는 경우도 있는데.. 그러면 매 API 요청마다 토큰 검증이 필요한 게 아닐까? 하는 생각도 든다. 최초 로그인 후 Role에 따라 권한을 부여해주면 로그인이 유지되는 동안은 이 유저에 대한 토큰 검증을 다시 안 하는 건지? 그래도 보안이 유지되는지 궁금하다.
Q5. UsernamePasswordAuthenticationFilter 이전에 필터를 배치한 이유는 무엇일까?
A. JwtFilter가 유저가 갖고 있는 토큰이 정상적인 토큰인지를 먼저 검증해, API를 요청하는 유저가 유효한 유저인지를 먼저 검증할 필요가 있다. 근데 생각해보니까 허용된 API외의 모든 요청에 JwtFilter 검증과 UsernamePasswordAuthenticationFilter가 이루어지는 것일까? 아니겠지? Login에서만 하는 거겠지? 그러면 Login과 Login 이외의 API에 대한 필터 코드를 다르게 작성해야 하는 건가?
- 아! JwtFilter는 "로그인된 사용자"가 보내는 모든 요청의 JWT를 검증하고, UsernamePasswordAuthenticationFilter는 "로그인 시" username/password를 처리하는 필터네! 그러니까, JwtFilter는 로그인 API에는 필요가 없다! 그냥 넘겨주는 것.(return ;)
- 전체 흐름 정리
- 4. @Auth Annotation 피드백
Q1. Spring Security의 AuthenticationPrincipalArgumentResolver와 기존의 커스텀 Resolver는 어떤 차이가 있었는가?
A. 잘 모르겠다. @Auth AuthUser 이렇게 썼었는데.. 이렇게 되면 AuthUser 클래스를 정의해줘야하고, @Auth 인터페이스도 커스텀 해줘야 하는건가? 이 과정이 커스텀 Resolver와 관련이 있는 것 같다. AuthenticationPrincipalArgumentResolver는 AuthenticationPrincipal로 대체해 사용할 수 있었던 것 같다.
- 이걸 보면, Spring 방식이 훨씬 좋아 보이는데?! 먼저 @Auth를 사용해서 친숙하게 느껴지는 것일뿐, Spring 방식(@AuthenticiationPrincipal)이 훨씬 좋아 보인다.
- 아! @AuthenticationPrincipal을 쓰면, Authentication.getPrincipal()의 결과를 자동으로 주입해주는 AuthenticationPrincipalArgumentResolver를 제공하는구나! 별도의 구현 없이도 쉽게 인증 객체를 받을 수 있어 간결하다고 한다!
- 그리고, 커스텀 방식은 @Auth 같은 어노테이션과 함께 HandlerMethodArgumentResolver를 직접 구현해서 인증 정보를 매개변수에 주입하는 방식이란다. HandlerMethodArgumentResolver를 직접 구현하느냐, 자동으로 주입해주느냐의 차이네. @AuthenticationPrincipal이 훨씬 편할 것 같다!
Q2. AuthUser annotation은 어떻게 작동하는가? - @AuthenticationPrincipal로 대체 가능했는가?
A. AuthUser의 정보를 바탕으로 DB조회 등을 진행했다. 대체가 가능했으나, 반환 서비스단에서 AuthUser를 활용하고 있는 메서드들이 많아, Principal을 AuthUser로 변환해서 사용했다. 처음부터 Principal을 사용하도록 설계했으면 더 편했을 것 같다.
Q3. 커스텀한 Resolver를 쓰던 것을 Spring Security 방식으로 통합할 때 어떤 장점이 생겼나?
A. 사실 이 부분은 잘 모르겠다. 아마 구조를 제대로 파악하고 쓰지 않고 보고 따라하는 방식으로 했기 때문이 아닐까 싶다. 이 부분은 구체적으로 코칭을 받고 싶다.
Q4. 권한을 어떻게 주입받고, 어떤 형태로 컨트롤러까지 도달하는가?
A. Spring Security 필터를 거치면 UserDetailsService가 SecurityContextHolder에 유저 정보를 넣어준다. 이 SecurityContextHolder에 있는 정보가 컨트롤러로 전달되는 것 같다.
- 실무적으로 추천하는 설계
- 일반적으로는 UserDetails를 사용하지만, 커스텀 필드가 필요하면 CustomPrincipal로 받아서 사용하는구나!
- Q. UserDetails에는 어떤 정보가 있고, CustomPrincipal은 어떤 정보가 있고 어떻게 설계하지?
- Spring Security에서 인증된 사용자 정보를 표현하는 인터페이스구나! Spring Security는 로그인 성공 후 UserDetailsService에서 반환한 객체를 기준으로 인증을 처리한다!
- CustomPrincipal은 UserDetails를 상속한다! 추가로, Authentication.getPrincipal()로 반환되는 객체를 우리가 원하는 정보로 구성한 사용자 정보 객체라고! 결국 UserDetails의 일종이구나! 인증(로그인)에는 UserDetails가 필요하고, 비즈니스 로직이나 컨트롤러에서는 CustomPrincipal이 더 편리하다고 한다! 와 이해됐다!
- UserDetails와 CustomPrincipal는 어떻게 동작하는가?
- 설계 전략
- CustomPrincipal은 반드시 써야할까? 설계 시 고려할 질문
- 오! 이거에 따르면 CustomPrincipal을 사용하면 사용자의 정보를 DB 조회 없이 자주 꺼내쓸 때 더 효율적이라고 하는 것 같은데!
- 설계는 위와 같이! 네 가지 클래스 정의가 필요하구나!
- CustomPrincipal 클래스 설계 예제를 보자.
- 왜 Getter를 쓰지 않냐고 물어보니, UserDetails 인터페이스 자체에 getUsername(), getPassword() 메서드가 정의되어 있어 오버라이딩이 필요하다고 한다. 오버라이딩이 필요한 이유는, 이 오버라이딩 된 메서드를 Spring Security가 내부적으로 호출하기 때문이라고! getUsername(), getAuthorities()는 각각 로그인 아이디로 사용되고, 인가 처리를 위해 호출된다.
- 즉, Spring Security가 Authentication.getPrincipal()에서 해당 객체를 꺼낸 후, 내부적으로 이 메서드들을 호출해서 권한 체크와 인증 처리를 수행하므로, 이 메서드들을 오버라이딩하지 않으면 인증/인가가 제대로 작동하지 않는다!
- 정리
- 다음으로 JWT인증 흐름 도식 살펴보자. 보니까, UserDetailsServiceImpl 클래스도 구현해야 하는 듯.
[Client] | | 1. 로그인 요청 (ID/PW) v [AuthenticationController] --- calls ---> [UserDetailsServiceImpl.loadUserByUsername()] | | | v | [CustomPrincipal 반환] v [AuthenticationManager / Provider] | | 2. 인증 성공 v [SecurityContextHolder] | | 3. JWT 발급 (JwtUtil.createToken()) v [Client] ←←←←←←←←←←←←←←←←←←←← [JWT] | | 4. 모든 요청에 JWT 포함 v [JwtFilter] → 검증 → [SecurityContextHolder] | | 5. @AuthenticationPrincipal CustomPrincipal v [Controller]
- 다음은 UserDetails 사용 시와 CustomPrincipal 사용 시 비교
- UserDetailsServiceImpl도 살펴보자! UserDetailsService를 상속 받는구나! 흐름 상. 이 친구가 UserDetails 혹은 CustomPrincipal을 반환해준다!
- 다음은 JwtFilter 예시를 살펴보자. JwtUtil과 UserDetailsService를 속성으로 주입받고 있다. doFilterInternal에서 짤린 필드는 FilterChain이다.
- 아래는 SecurityConfig 예시. JwtFilter를 속성으로 주입받고 있다. SecurityFilterChain을 반환하는데, UsernamePasswordAuthenticationFilter 이전에 jwtFilter를 붙여라고 명령해주는 것 같다. -> 맞다고 한다!
- 5. CustomPrincipal 피드백 ... UserDetails는 인증 객체의 핵심 인터페이스라고 함! CustomPrincipal은 결국 UserDetailsService 구현체에서 반환되는 사용자 정보의 구체화된 형태라고!
Q1. 왜 UserDetails를 직접 구현한 커스텀 클래스를 만들었는가?
A. UserDetails에는 username(아이디), password, authorities(role)과 같은 최소한의 정보만 담겨 있어 비즈니스 로직에서 필요한 유저 정보를 활용하기 어렵다. 따라서 UserDetails를 직접 구현한 CustomPrincipal 클래스를 구현해 비즈니스 레이어에서 필요한 필드와 기능들을 정의해주었다.
- 아! 인증 외 로직에서도 토큰 없이 사용자 정보를 쉽게 참조할 수 있게 된다고 한다! 근데 아까 토큰은 매 요청마다 검증한다고 했는데, 매번 토큰에서 유저 정보를 뽑는 것이 아니라, 받아온 CustomPrincipal 인스턴스에서 유저 정보를 뽑을 수 있다는 말인가?
- 유저 정보는 한 번만 파싱해서 SecurityContext에 저장해놓는다고! 좀 더 이해된 느낌이다! 아래의 핵심 포인트도 참고!
- 실제 JwtFilter 안에서는 이렇게 정의한다.
- 아 그럼 OncePerRequestFilter를 상속한다는 말은, 이 토큰 파싱을 한 번만 한다는 말인가? 아! 그게 아니라, 여러 필터가 존재해도 JwtFilter는 한 번한 실행한다는 말이네!
- 그러면, 매 요청마다 토큰을 파싱해서 SecurityContextHolder를 세팅하는 거야? 토큰이 유효한 것이 검증되면 기존에 SecurityContextHolder에 저장했던 UserDetails를 사용할 수 있는게 아니고? 이게 어떤 이점이 있는지 모르겠다.
- JWT 기반 인증은 stateless하기 때문에 매 요청마다 토큰을 파싱하고 SecurityContextHolder를 다시 세팅해야한다고 한다!
- 음.. 정리된 건 알겠는데, 좀 더 알아보자.
- 요청 때마다 SecurityContextHolder에 CustomPrincipal을 세팅해준다는 거네. 요청 종료 시 비워지고.
- 이런 장점이 있어, 매 요청마다 세팅을 다시 해주는구나.
- SecurityContextHolder는 비워줘야한다! 그러니까 요청 때마다 다시 파싱 후 UserDetails를 만들어 줘야한다! 이것도 Stateless한 방식의 일부라고!
- Q. 그러면 DB를 조회하지 않고 UserDetails를 활용할 수 있다는 말이, UserDetails를 찾을 때도 db조회를 하는데, 서비스나 컨트롤러 레이어에서 한번 더 할 필요가 없어서 효율적이라는 말인가? -> 맞다고 한다!
- 이해했다!
Q2. 이 클래스는 어떤 필드를 가지고 있었고, 그것들이 인증 이후 어떻게 활용되었는가?
A. username, password, authorities를 기본으로 갖는다. 로그인 시 UsernamePasswordAuthenticationFilter에서 일치 여부를 확인할 때 사용하고, 이후에는 JWT를 활용할 경우 토큰만 검증해 SecurityContextHolder에 담긴 UserDetails 정보를 넘겨줘 비즈니스나 컨트롤러 레이어에서 활용하도록 한다.
- 아! UsernamePasswordAuthenticationFilter와는 관련이 없구나! 흐름 상 이미 인증에 성공해 CustomPrincipal을 반환하는 거니까 이후에 활용되는 얘기를 하는게 맞겠네. API 요청마다 JWT 토큰을 파싱하고, UserDetails 정보를 SecurityContext에 넣어주고 활용 시엔 @AuthenticationPrincipal UserDetails(CustomPrincipal)로 꺼내 쓰면 되는 듯! 별도 설정 없이!
- UsernamePasswordAuthenticationFilter와 CustomPrincipal의 연결이 JWT 흐름에서는 직접적인 연관이 없다고 함!
- 잠깐만, 그럼 UsernamePasswordAuthenticationFilter는 UserDetails를 받는게 아닌가? 어떻게 동작하지?
- 구체적으로,
- 아! 맞다! UsernamePasswordAuthenticationFilter는 로그인 시에 인증을 하는 필터고, 로그인 이후에는 JWT를 사용한다고 했지! 금붕어다ㅋㅋ
- SecurityResponseDto 피드백
Q1. 이 DTO는 주로 응답(Response)에 쓰기 위한 것인가, 인증 과정 중 내부 전파용인가?
A. 응답에 쓰기 위한 용도다. 인증 과정 중 내부 전파 용도는 UserDetails로 충분하다.
- 내용을 점점 이해해가고 있다!
Q2. SecurityContextHolder.getContext().getAuthentication()에서 꺼내 쓸 수 있는 정보와 DTO를 따로 만드는 이유는?
A. 그러게. 왜 따로 만들었을까? 생각해보면 분리할 필요 없이 CustomPrincipal에서 필요한 정보들만 빼서 써도 되었을 것 같은데, CustomPrincipal에서 일부 필드만으로 편집한 DTO이고, 활용이 많다면 분리하는 것도 고려해볼 수 있겠다.
- 어라. 분리가 더 바람직하다고 한다. CustomPrincipal은 보안+인증 로직 전용, SecurityResponseDto는 응답 전용으로, API에 맞춰 필드를 가공해 제공한다고 한다.
Q3. 도메인 모델과 응답 모델을 분리하는 이유는?
A. 그러게. 왜 분리했을까. 동일하게 가져가고 필요한 정보들만 그때그때 따로 써도 될 것 같은데.
- 실무에서는 분리하는 것이 모범사례! 도메인 모델은 DB와 밀접하게 연결. 응답 모델은 클라이언트 요구사항에 맞춰 구성. 관심사와 책임을 분리하는 것이 유지보수성과 안정성을 높이는 실무 패턴!
Q4. 이 DTO는 어떤 시점에, 어떤 계층(Controller/Service)에서 활용되었는가?
A. 인증이 끝난 이후, Service 레이어에서 해당 정보로 비즈니스 로직을 수행할 때 활용되었던 것 같은데, 생각해보면 용도가 명확하지 않았던 것 같다.
- 아~ Service 레이어에서는 CustomPrincipal을 그대로 받아 사용하는 것이 자연스럽고, DTO는 응답 구조를 정의하는 책임만 가지도록 Controller에서 DTO를 사용! Service 레이어는 DTO로 반환만 해주는 거지!
Security 활용
- Spring Security란?
- Session / JWT 등 인증을 위한 정보를 활용할 수 있도록 만들어주는 기술
- JWT 기반 인증 구조 개요
- JwtFilter가 SecurityFilterChain 중 UsernamePasswordAuthenticationFilter 앞에 위치해 로그인 이외의 API 요청에 대해 사용자의 토큰을 검증하고 Spring Security를 활용해 인증/인가 필터 진행
- 주요 구성 요소 ('JwtFilter', 'CustomPrincipal', 'UserDetailsServiceImpl' 등)
- JwtFilter 안에서 JwtUtil를 호출하고, CustomPrincipal은 UserDetails의 구현체로, 비즈니스 로직에서 유저 정보를 DB 조회 없이 활용할 수 있는 구성 요소이며, UserDetailsServiceImpl은 로그인 레벨에서 DB에서 조회한 유저 정보를 SecurityContext에 넣어주는 역할을 함.
- 인증 흐름 도식 (로그인/ 요청 처리)
- 로그인 시: 요청 > SecurityFilterChain(UsernamePasswordAuthenticationFilter 등) > API응답
- 로그인 후 : 요청 > SecurityFilterChain(JwtFilter 등) > API 응답
- 인증 객체 vs 응답 DTO 분리 이유
- 인증 객체(UserDetails or CustomPrincipal)의 경우 응답에 필요하지 않은 유저 정보를 담고 있을 수 있으므로, 필요한 정보만 뽑아 응답 DTO로 분리하는 것이 적절. 관심사 분리에도 적합한 방법임.
- 설정 및 구현 중 겪은 시행착오
- 우선 구현해야할 클래스는 1) JwtUtil 2) JwtFilter 3) UserDetailsServiceImpl 4) CustomPrincipal 5) SecurityConfig이고, 기존에 @Auth로 CustomArgumentResolver로 구현한 파트를 덜어내고, @Auth 대신 @AuthenticationPrincipal UserDetails(or CustomPrincipal) 인증 객체를 받아올 수 있음
- 배운 점 및 향후 개선 방향
- GPT를 보고 구현하니 어찌저찌 돌아갔지만, 내용을 정리해보니 전체 흐름과 구성요소를 제대로 파악하지 못했음을 알게 되었다. 그리고 중간에 모르는 내용이 나왔을 때 그냥 넘어가면, 가장 중요한 핀이 빠져 전체적인 이해를 어렵게 만들어주는 것을 알게 되었다. 이해가 안 가는 부분이 있으면 그때 직접 GPT한테 물어보는 등의 방법으로 이해를 하고 넘어가야하고, 그 과정에서 100으로 보였던 구멍의 크기가 점점 채워져, 뒤로 갈수록 질문들에 답을 하기가 수월해진다. 기술을 가져와 쓰는 것도 중요하지만, 내부의 구성 요소들이 어떻게 되는지 이해하는 과정이 매우 중요함을 느꼈다. 속도에 집착하지 말고, 제대로 이해하고 넘어가자.