인증이란?
- 요청 보낸 사람이 진짜 로그인 한 유저인지 확인하는 절차이다.
- 스프링 시큐리티에서는 Authentication 객체로 이걸 관리한다.
- 이 객체에는 유저, 이름, 인증되었는지에 대해 담겨있다.
SecurityContext/ SecurityContextHolder
- SecurityContext : 현재 로그인 한 사용자의 인증 정보를 담고 있다.
- SecurityContextHolder: 이 SecurityContext를 저장하는 static 클래스이다.
- SecurityContextHolder는 내부적으로 ThreadLocal을 쓴다.→ ThreadLocal은 각 스레드가 자기만의 변수를 가질 수 있도록 하는 자바 클래스이다. 스프링 시큐리티는 여기에 SecurityContext를 저장한다. 그래서 동시적으로 요청해도 사용자의 정보가 섞이지 않는다.
- → 요청마다 스레드가 다르고, 그 스레드 별로 인증정보를 따로 들고 있다.
JWT 기반 스프링 시큐리티 인증 흐름
- 스프링 시큐리티는 JWT를 공식적으로 지원하지 않고, 기본 로그인 방식은 UsernamePasswordAuthenticationFilter 에 세션을 저장하는 방식이다. 그래서 jwt 관련 로직은 직접 구현해야한다.
- 요청 수신 및 필터 체인 진입
- 클라이언트 요청이 들어오면, JWT는 보통 Authorization 헤더에 담겨서 전달된다.
- 요청은 OncePerRequestFilter(요청 당 한 번만 실행하도록 함)을 상속한 JwtAuthenticationFilter에서 먼저 잡힌다.
- 토큰을 추출하고 블랙리스트인지 확인한다.
- Authorization 헤더에서 Bearer 토큰을 추출한다.
- Bear 토큰 형태인 건 HTTP 인증 방식 중 하나인데, Bearer 붙이면 이 토큰을 가진 사람에게 자격이 있다는 의미가 담긴 것이다. 보통 jwt 토큰을 사용할 때 Bearer을 붙인다.
- 그다음에 레디스에 저장된 블랙리스트 토큰인지 확인한다.
- 로그아웃하거나 회원 탈퇴하면 블랙리스트에 저장한다. 토큰에 설정해놓은 만료기간이 끝날 때까지 기다려야하기 때문이다.
- Authorization 헤더에서 Bearer 토큰을 추출한다.
- 토큰 유효성 검증
- JwtProvider에서 해당 토큰이 유효한 토큰인지 검증한다. (만료시간과 서명)
- 스프링 시큐리티는 인증 안 된 사용자 요청과 인증은 되었지만 권한이 부족한 경우에 대해서 기본 핸들러가 있지만, 이는 웹페이지용 에러 응답이기 때문에 API 서버에서는 사용할 수 없다. 그래서 필터에서 JWT 예외를 직접 잡고, 이를 catch해서 JSON 포맷으로 응답을 내려주는 커스텀 예외 클래스를 구현한다.
- 인증 객체 생성
- 토큰에 있는 유저 정보와 역할 정보를 디코딩해서, UsernamePasswordAuthenticationToken 객체로 만들어준다.
- Authenticaion 인터페이스를 구현한 여러 클래스 중에 UsernamePasswordAuthenticaion 클래스가 있다. 이건 비밀번호가 필요없는 jwt 인증상황에서도 쓰인다. (비밀번호는 null로 처리한다.)
- 스프링 시큐리티는 이 객체 안의 유저정보, 권한, 인증 여부를 가지고 인증 상태를 판단한다.
- 나는 토큰에 권한도 추가했다. SimpleGrantedAuthority는 GrantedAuthority의 구현체인데, 한 명의 유저가 여러개의 권한을 가질 수 있기 떄문에 리스트로 구성한다. 이 SimpleGrantedAuthority 리스트들은 UsernamePasswordAuthentication생성할 때 세 번째 인자로 들어간다.
- UsernamePassWordAuthentcation의 첫 번째 인자는 principa(인증된 사용자 정보)l로써 userId가 저장된다.
- 토큰에 있는 유저 정보와 역할 정보를 디코딩해서, UsernamePasswordAuthenticationToken 객체로 만들어준다.
- 만든 Authentication 객체를 SecurityContext에 등록한다.
- 만들어진 인증 객체를 현재 요청의 securityContext에 등록한다. (그러면 이건 securityContextHolder에서 threadLocal의 형태로 관리된다.)
- 요청이 끝나면 자동으로 securityContext가 정리된다.
- ThreadLocal에 저장된 인증 정보는 요청이 끝나면 자동으로 제거된다.
- 스프링 시큐리티는 기본적으로 SecurityContextPersistenceFilter를 가지는데 이건 요청 시작할 때, ContextHolder에 컨텍스트를 등록했다가 요청 끝나면 자동으로 clear() 해준다. 이 게 없으면 ThreadLocal에 사용자 정보가 계속 남아있어서 메모리 누수 일어날 수 있다.
- 그래서 메모리 누수 방지와 유저 간 인증정보는 섞이지 않는다.
- ThreadLocal에 저장된 인증 정보는 요청이 끝나면 자동으로 제거된다.
- 인증된 사용자 정보 꺼내는 방식
- SecurityContextHolder의 getContext()의 getAuthentication()에서 getPrincipal을 통해서 userId를 가져온다. 이를 디비에서 사용자 아이디가 존재하는지를 검증해서 유저 정보 가져오는 방식이다.
최종 정리
- Authorization 헤더에서 Bear가 붙은 jwt 토큰을 추출한다.
- 이후에 해당 토큰이 블랙리스트에 등재되었는지 확인하고, 없으면, 이에 대해 서명과 만료 정보 등을 검증한다. 이 과정이 끝나면, 권한(SingleGrantedAuthority 리스트) 과 유저 아이디가 포함된 authentication 객체를 만들어서 securityContext에 등록한다. 그러면 securityContext는 SecurityContextHolder에 등재되어 threadLocal로 독립적으로 관리되기 때문에, 동시요청에도 유저 정보가 섞일 걱정을 하지 않아도 된다. 서비스나 컨트롤러에서 유저 정보가 필요하면 시큐리티 컨텍스트 홀더의 시큐리티 컨텍스트의 principal을 가져와서 디비에서 유저 추가 정보 가져오는 방식으로 사용된다. 이는 스프링 시큐리티의 securityContextPersistenceFilter에 의해 요청이 종료되면 컨텍스트는 자동으로 클리어 되어 메모리 누수 방지한다. (클리어 과정은 자동으로 된다.)
'프로젝트 정리' 카테고리의 다른 글
WebDriverWait vs pageLoadTimeout (0) | 2025.01.27 |
---|---|
OOM SCORE 점수란? (0) | 2025.01.11 |
Docker 볼륨과 EBS 볼륨 차이 (0) | 2024.12.21 |
깃허브 액션과 슬랙 연동해서 배포 결과 알림 받기 (0) | 2024.12.20 |
aws 대상그룹 Unhealthy Health checks failed with these codes: [403] 문제 해결 방법 (0) | 2024.12.18 |