일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- Spring Security
- OOP
- RDS
- OSI 7계층
- 형상관리
- JMeter
- 3-way-handshake
- 인메모리
- 성능 개선
- N+1 문제
- 트러블 슈팅
- 코딩테스트
- docker
- TCP/IP
- 알고리즘
- AWS
- 동시성 문제
- 로그인
- Spring
- EC2
- 프로토콜
- JWT
- redis
- 백준
- 4-way-handshake
- 스프링부트
- 객체지향
- Java
- 네트워크
- Today
- Total
열공스토리
[Spring] Security + JWT 로그인 인증 방식 내부 동작 과정 본문
최근 Spring Security와 JWT를 이용한 로그인 인증 방식에 대해 좀 더 깊이 공부 해 보면서 처음 JWT 인증 방식에 대해 공부할 때 광범위한 개념들과 복잡한 동작 흐름을 이해하기 힘들어 했던 저와 같은 처지에 있는 분들이 이해하는데 도움이 되었으면 하는 바램으로 포스팅하게 되었습니다.
들어가기에 앞서, 이번 포스팅에서는 토큰 발급과 인증을 하는 과정에서 등장하는 개념들과 동작 흐름의 이해를 중점으로 다룰 예정이므로 선수 지식(JWT 구성 등)은 생략하겠습니다.
개요
저는 처음 Security와 JWT를 이용한 로그인 인증 방식에 대해 공부 할 때 서버 내부에서 어떻게 실행이 이루어지는지가 머리에 잘 안들어 오더라구요. 새로운 객체나 클래스, 메소드를 자꾸만 접하다보니 머릿 속이 혼라스러워서 그랬던거 같아요 :(
동작의 흐름은 크게 두 가지로 나눠볼 수 있는데요. 첫 번째는 클라이언트로부터 로그인 요청이 들어왔을 때 (로그인), 두 번째는 발급 받은 토큰을 가지고 클라이언트로부터 요청이 들어왔을 때 (토큰 인증)로 나눠볼 수 있습니다.
로그인
첫 번째, 클라이언트로부터 로그인 요청이 들어왔을 때는 email과 password를 가지고 요청이 들어오면 결과적으로 서버에서는 email과 password를 가지고 발급한 access token을 반환해 줍니다. 이러한 과정이 서버 내부에서는 다음과 같이 동작하게 됩니다.
[알아둬야할 개념]
1. UsernamePasswordAuthenticationToken
Authentication이라는 인터페이스를 구현한 구현체로 간단히 하면 클라이언트의 정보(사용자 객체, 권한 정보 등)를 담은 '인증 객체'이고 앞으로 이 인증 객체를 가지고 토큰 발급을 하거나 사용자 정보를 가져오거나 할 예정이므로 인증 객체하면 'UsernamePasswordAuthenticationToken'으로 인지하고 있으면 된다. (Spring Security에서 제공)
2. AuthenticationManager
이 클래스에서는 딱 하나만 알면 되는데 그건 바로 authenticate() 라는 내부 메소드이다. 인자로 위의 '인증 객체'를 넣어 호출하게 되면 AuthenticationManager가 등록된 AuthenticationProvider 목록을 순차적으로 확인하며, 적합한 AuthenticationProvider를 찾아 AuthenticationProvider의 authenticate()를 호출 한다.
3. CustomAuthenticationProvider
AuthenticationProvider를 구현한 구현체로 위의 AuthenticationManager의 authenticate()메소드를 통해 호출되면 내부 메소드인 authenticate()가 호출 된다. 그리고 이 메소드에서 파라미터로 받은 '인증 객체'와 실제 레포지토리에 저장된 사용자 정보를 비교하여 사용자가 정보가 일치하다면 앞으로 jwt토큰을 생성하거나 인증 등에 사용되는 디테일한 사용자 정보를 담은 실질적인 '인증 객체'를 반환한다. (개발자 직접 구현)
4. CustomUserDetailsService
UserDetailsService를 구현한 구현체로 CustomAuthenticationProvider 등에서 사용되고 레포지토리에서 저장된 유저의 정보를 가져와 UserDetails 스펙에 맞게 객체를 생성하여 반환하는 정도의 용도라고 보면 된다. (개발자 직접 구현)
5. CustomUserDetails
위 CustomUserDerailsService 설명에 나오는 UserDetails를 구현한 구현체이고 '인증 객체'에 담겨지는 유저 정보에 대한 데이터를 개발자가 원하는대로 직접 구현할 수 있다. (개발자 직접 구현)
(위 설명들 중에서 'Custom'이라고 붙여진 개발자가 직접 구현해야 하는 클래스들은 굳이 구현하지 않아도 동작하지만 구현하지 않는다면 각각 Spring Security에서 지원하는 스펙에 맞춰서 구현해야한다. 이 포스팅에서는 위 설명대로 직접 구현을 하여 진행했기 때문에 커스텀 클래스를 구현했다는 전제로 진행할 것이다.)
[동작 과정]
1.email과 password를 가지고 UsernamePasswordAuthenticationToken 타입의 '임의의 인증 객체'를 생성한다.
2.AuthenticationManagerBuilder의 getObject() 메소드로 AuthenticationManager 객체를 가져와 내부 메소드인 authenticate()를 위에서 생성한 '임의의 인증 객체'를 인자로 넣어 호출한다.
3.AuthenticationManager는 등록된 AuthenticationProvider 목록을 순차적으로 확인하여 적합한 AuthenticationProvider를 찾아 AuthenticationProvider 내부의 authenticate()를 호출한다. (AuthenticationProvider를 구현한 클래스가 있을 경우 그 클래스가 choice됨)
4.이 때, AuthenticationProvider의 authenticate() 메소드에서는 파라미터로 '임의의 인증 객체'를 받게 되는데 이 인증 객체를 이용하여 CustomUserDetailsService 내부의 loadUserByUsername()이라는 메소드에 인증 객체에 담은 email을 인자로 넘겨 주어 호출한다.
5.loadUserByUsername()에서 레포지토리에 email과 일치하는 사용자 객체를 가져와 CustomUserDetails 객체로 반환한다.
6.AuthenticationProvider에서 반환받은 CustomUserDetails의 password와 '임의의 인증 객체' 담긴 password가 일치한지 검사한다.(보통 비밀번호는 암호화하여 레포지토리에 저장하기 때문에 암호화 객체로 PasswordEncoder를 사용한다면 PasswordEncoder의 matchs() 메소드를 사용해 보세요!)
7.비밀번호가 일치한다면 UsernamePasswordAuthenticationToken에 CustomUserDetails와 password 그리고 customUserDetails의 권한 정보를 담아 '실질적인 인증 객체'를 생성하여 리턴한다.
8.이제 TokenProvider라는, jwt 토큰에 대한 다양한 작업(인증 객체로 jwt 토큰 생성, jwt 토큰으로부터 인증 객체 빼오기 등)이 가능한 유틸성 클래스의 createToken() 메소드에 '실질적인 인증 객체'를 인자로 넣어 호출하여 인증 객체의 사용자 정보와 유효기간을 설정하여 생성한 jwt 토큰을 리턴 받는다.
9.이 jwt 토큰과 함께 클라이언트에게 응답하면 끝. (클라이언트는 이 토큰을 내부에 저장하여 이 후 요청을 보낼 때마다 http header에 함께 보내어 토큰 인증을 받게 된다.)
로그인 과정에서는 로그인 요청 시, 사용자의 email과 password를 가지고 '임의의 인증 객체'를 생성하여 내부적으로 '실질적인 인증 객체'를 만들고 이 '실질적인 인증 객체'를 가지고 jwt 토큰을 만들어 결과적으로 토큰을 클라이언트에게 반환합니다. 이 때, 클라이언트에서만 이 토큰을 저장하고 서버에서는 토큰을 저장하지 않는다는 정도로 정리하면 되겠습니다.
토큰 인증
다음 두번 째는 발급 받은 토큰을 가지고 클라이언트로부터 요청이 들어왔을 때 토큰 유효성 검사를 통해 이 토큰이 유효한지 확인하고 토큰으로부터 사용자 정보를 빼와서 사용자를 식별하여 결과적으로 api 요구에 맞게 로그인한 사용자의 데이터를 반환해 주게 됩니다. 이러한 과정이 서버 내부에서는 다음과 같이 동작하게 됩니다.
[알아둬야할 개념]
1. JwtFilter
GenericFilter를 구현한 구현체이고 'Filter'는 http 요청이 들어올 때 서블릿(api)에 도달하기 전 요청과 응답에 추가적인 작업을 해야할 때 거치는 단계로 여기서는 header에 담긴 토큰의 유효성 검사와 토큰으로부터 '인증 객체'를 추출하여 SecurityContext에 이 인증 객체를 저장하는 작업을 수행한다.(개발자 직접 구현, *필수)
2. SecurityContext
SecurityContextHolder의 getContext()로 객체를 가져올 수 있고 setAuthentication() 메소드에 위에서 추출한 인증 객체를 담아 저장하는 용도로 사용된다.
3. @AuthenticationPrincipal
controller에서 사용되는 어노테이션으로 SecurityContext에 저장한 Authentication 객체의 principal을 가져온다. (여기서 말하는principal이란 위 로그인 과정에서 '실질적인 인증 객체'를 생성할 때 첫 번째 인자에 담은 CustomUserDetails 객체이고 이는 즉, principal을 뜻한다. 따라서 이 어노테이션으로 유저 정보 가져올 수 있다.)
4. @PreAuthorize
유저의 권한에 따른 api 실행 여부를 설정하는 어노테이션으로 '실질적인 인증 객체'의 세번 째 인자로 생성된 유저의 권한정보와 비교하여 권한이 맞다면 실행될 수 있도록 한다. (유저 권한정보는 Filter에서 SecurityContext에 담은 인증 객체에 접근하여 권한이 맞는지 확인한다. 꼭 적용해야하는 요소는 아님)
[동작 과정]
1.클라이언트로부터 http 요청이 들어오면 JwtFilter에서 헤더에 담긴 access 토큰을 jwt 토큰 형식에 맞게 풀어 저장한다.
2.저장한 토큰의 유효성을 검사한다.
3.그리고 저장한 토큰을 위에서 언급한 토큰의 다양한 작업이 이루어지는 TokenProvider 클래스의 getAuthentication() 메소드 인자에 담아 호출하여 '인증 객체'를 추출한다.
4.리턴 받은 '인증 객체'를 SecurityContext에 setAuthentication() 메소드의 인자로 담아 호출하여 저장한다.
5.controller로 넘어와 @PreAuthorize에 설정된 권한과 일치하는지 검증받고 @AuthenticationPrincipal을 CustomUserDetails 객체로 받아와 결과적으로 현재 로그인한 유저에 대한 작업이 이루어지게 된다.
정리
처음 jwt 토큰 인증방식에 대해 공부할 적에는 인증 과정에서 대부분이 jwt 토큰에 의해 인증이 이루어진다고 생각해서 jwt 토큰을 주요한 요소로 인지하면서 공부했는데 막상 제대로 공부해보니 jwt 토큰은 단지, 클라이언트에게 클라이언트 자기 자신의 데이터 정보를 암호화하여 서버에 인증하기 위해 주고 받는 '문자열' 정도로만 사용되고 있었고 실상 서버 내부에서는 Spring Security에 의해 인증에 대한 대부분의 작업이 이루어지고 있었다는 사실을 깨닫게 되었습니다. 그래서 처음 잘못된 선입견을 가져서 이해하는데 오래걸린 것도 같아 조금 후회되기도 합니다만 그래도 어쨋든 결과적으로 흐릿하게 알고 넘어갔던 인증과정에 대해 확실히 이해가 된 것 같아 뿌듯하네요.
Spring Security + jwt를 이용한 인증 방식에 대해 저와 같이 이해의 어려움을 겪은 분들이 조금이나마 도움이 되셨으면 좋겠네요! 그럼 이만 글 줄이겠습니다:)
(제가 공부하면서 구현한 실제 코드들은 깃허브에 올려두었습니다. main 브랜치에는 현재 redis repository를 이용하여 refresh token 인증 방식을 추가하였고 블랙리스트를 이용한 로그아웃 기능도 추가한 상태인데, access 토큰만 구현한 코드를 보고 싶으시면 feature 브랜치를 확인해 주세요.)
github : https://github.com/xogus3492/jwt/tree/main
'Spring' 카테고리의 다른 글
[Spring] 이메일 발신 기능 성능 개선 (0) | 2024.09.24 |
---|---|
[Spring] 파일 저장 로직의 동시 요청에 의한 동시 저장을 방지하는 방법 (0) | 2024.08.17 |
[Spring] 게시판 좋아요 기능 동시성 문제 트러블 슈팅 (2) | 2023.07.28 |
[Spring] N+1 문제 트러블 슈팅 (0) | 2023.07.28 |
[Spring] Redis 사용 해 보기 (0) | 2023.07.28 |