WAS 상 사용자 데이터 관리
WAS에서 서버의 회원의 데이터를 안전하게 관리하기 위해서
서버에서 사용자의 요청을 검증하는 과정이 필요하다고 생각했습니다.
쉽게 말해서, 서버에서 해당 사용자가 서버에 등록된 사람인지(인증)와
이 인증된 사용자가 서버 내 특정 데이터에 접근 or 특정 작업을 수행할 수 있는 권한을 가지고 있는지(인가)를 확인하는 과정은 필수적이라 판단했습니다.
Session, JSON Web Token(JWT) TradeOff
인증과 인가에는 크게 Session 방식과 JWT 방식이 있음을 확인했습니다.
하지만, 이 두 가지 방식에 뭐가 더 좋다라고 판단하기 어려웠습니다.
따라서, 두 가지 방식과 특정 방식을 선택했을 때 어떤 트레이드 오프 과정을 거칠지 판단할 필요가 있겠다 생각했습니다.
Session 방식
- 작동 방식: 클라이언트가 로그인하면 서버에서 세션 ID를 생성하고 이를 쿠키에 저장하여 클라이언트에 반환합니다. 이후 클라이언트의 요청마다 이 쿠키를 이용하여 세션 ID를 서버에 전송하고, 서버는 이를 확인하여 사용자를 인증합니다.
- 이점: 사용자의 인증 정보가 서버에 저장되므로, 클라이언트 측에서 접근이 불가능합니다. 이는 보안 상의 이점을 제공할 수 있습니다.
- 한계점: 사용자가 많아질수록 서버의 메모리 부담이 증가합니다. 때문에 WAS를 scale out 할 필요가 있는데, 이 때 세션이 하나의 WAS에만 저장이 되는 구조를 띄게 된다면, 사용자에 대한 인증, 인가 처리가 되지 않습니다.
따라서, 세션 상태를 관리하기 위한 별도 세션 저장소(ex. Redis)를 별도로 구축하거나 JWT 도입을 고민할 필요가 있습니다.
JWT 방식
- 작동 방식: 클라이언트가 인증을 요청하면, 서버는 특정 정보(페이로드)와 서명을 포함한 토큰을 생성하여 클라이언트에게 전달합니다. 클라이언트는 이후 요청마다 이 토큰을 함께 전송하며, 서버는 토큰을 검증(jwt 복호화, 서명의 Secret 확인)하여 사용자를 인증합니다.
- 이점: 상태를 유지하지 않아도 되므로 확장성이 뛰어납니다. 서버는 토큰만 검증하면 되기 때문에, 추가적인 인증 저장소가 필요 없으며, 분산 시스템에서의 인증에 적합합니다.
- 한계점: 토큰이 탈취되면 보안이 위협받을 수 있으며, 토큰의 길이가 길어질수록 네트워크에 부하를 줄 수 있습니다. 또한, JWT는 상태를 갖지 않기 때문에, 한 번 발급되면 만료 시간 전까지는 회수가 불가능합니다(즉, 로그아웃이 바로 적용되지 않을 수 있습니다).
개발 스펙
- Java(11)
- Amazon Corretto JDK(11)
- SpringBoot(2.7.14)
- Build(gradle)
- DB(MySQL)
내가 개발하고 있는 서비스가 어느 규모까지 성장할 수 있을까?
만약, 많은 사람들이 이용한다면 어떻게 구조를 짜야할까?? 를 진지하게 고민했습니다. 혼자 고민해서 인사이트가 부족한 점 양해부탁드립니다..
현재의 제 인사이트로는 아래와 같이 구현해야할 거 같습니다.
신경써야할 것
- 다중 서버에서 회원의 데이터를 안전하게 관리할 수 있어야 한다.
- 서버의 부하를 최소화시킬 수 있도록 해야한다.
- 만약 세션 이용 시, 다중 서버 환경에서 세션 정합성 관리를 해야한다.
인증 / 인가 방식 선택
제시해본 요구사항을 만족하기 위해서는 아래 선택지가 있겠다 판단했습니다.
1. Sticky Session 도입
2. WAS 서버를 2대 이상으로 늘려, WAS 세션 클러스터링 설정
3. 별도의 Redis 세션 저장소 구축
4. JWT 도입
1번 방식은 특정 WAS에만 과부하가 집중될 수 있고, 만약 해당 WAS가 뻗으면 WAS 내 세션들도 사라져 요구사항을 만족시킬 수는 없겠다 생각했습니다.
2번 방식은 세션 정합성 문제를 해결할 수는 있겠지만,
WAS 네트워크 트레픽의 증가와 함께 혹시나 모를 시간 차의 세션 불일치로 인해 데이터 정합성을 지킬 수 없겠다 판단했습니다.
따라서 1번 방식의 부하 몰림 문제와 2번 방식의 가용성 문제를 해결할 수 있기 때문에 3번 방식을 고민하기도 했습니다.
하지만, 현재 인증과 인가를 제대로 다루어 본적 없는 제 상태에서는 Redis에 대해서 깊게 파는 것에 시간을 투자하는 것보다
전체적인 사이클을 경험해보는 것에 더 중점을 두자고 생각했습니다.
또한, 3번 방식을 쓰더라도 하나뿐인 세션 스토리지가 먹통이 된다면 세션 데이터 자체를 정상적으로 사용할 수 없으며 이에 대해서도 더 파고드는 것은 현재 논점인 인증과 인가를 직접 구현해보겠다는 취지에도 맞지 않다고 생각했습니다.
반면, 4번 방식인 JWT 토큰은 중앙의 인증 서버에 대한 의존성이 없습니다. 따라서, 다중 서버를 위한 scale-out 측면에서 자유롭다고 판단했습니다.
단, JWT 토큰은 stateless를 띄고, 서버에서 토큰만 검증하기 때문에 탈취 될 경우를 대비해야 한다고 생각했습니다.
최대한 accessToken의 시간을 짧고 , refreshToken은 길게 가져가는 것으로 이를 예방하는 것으로 결정했습니다.
또한, 서버에서 회원의 데이터를 먼저 다루어 보는 것에 중점적으로 두자고 판단했습니다.
따라서, 요구사항을 충족시키는 최고의 선택으로 3번을 결정했습니다.
※ 몰랐던 키워드 설명
정합성 이란??
: 특정 데이터들의 값이 서로 일치하는 상태
위 상황에서는 다중 서버 환경에서의 WAS가 바라보는 세션의 값이 모두 일치하는 상태를 가리킵니다.
클러스터링이란??
: 군집이나 무리
세션 클러스터링이란??
: 즉, 여러 WAS의 세션을 하나로 묶어 하나의 세션으로 관리하는 것
-> 만약, 로드 밸런싱(대용량 트래픽 처리시 분산시키는 것) 또는 failover(장애 발생시 예비시스템으로 자동전환, 서버 이중화) 등에 의해 대체된 WAS 에게도 세션을 공유하게 해준다.
: 결국 모든 WAS가 동일한 세션 데이터를 바라보면서 데이터 정합성을 만족시키는 것
JWT(Json Web Token)
: Json 포맷을 이용해서 사용자에 대한 속성을 저장하는 Claim 기반의 Web Token 입니다.
그냥 Json 포맷이 어떻게 정보를 안전하게 전달 할 수 있는거지??
- JWT는 토큰 자체를 정보로 사용하는 Self-Contained 방식을 채택하고 있습니다.
Self Contained
- JWT는 기본적으로 세 부분 (header, payload, signature) 으로 구성되어 있습니다.
- header에는 토큰 타입이나 해싱 알고리즘 (HMAC, RSA)을 담습니다.
- payload에는 토큰에 담을 정보(claim)을 담습니다.
- signature에는 해당 토큰에 대한 서명을 담습니다.
위와 같은 구조로 인해 Self-Contained 라고 부르는 것 입니다.
토큰 자체가 이미 인증에 필요한 모든 정보를 담고 있으며 그에 대한 서버의 서명도 이루어져 있는 상태를 뜻합니다.
이 구조를 통해, 곧 JWT가 스스로 정보를 검증할 수 있는 구조를 뜻합니다!!
또한 이 구조 때문에 별도로 상태를 저장하지 않아도 되므로 Stateless라는 성질을 띄움으로써 중앙 서버에 대한 의존성을 제거해주는 것입니다.
JWT 전체 Flow
- 사용자가 로그인 시, 서버에서는 사용자 데이터를 안전하게 관리하기 위해 인증과정을 거칩니다.
- 일반적으로 먼저 DB에 접근해서, 먼저 사용자가 이미 서버에 등록된 회원인지 판단합니다.
만약, 회원으로 등록되어 있지 않을 시 JWT(accessToken, refreshToken)을 발급해줍니다.
회원으로 등록되어 있을 시, 회원이 등록되지 않습니다.
- refreshToken이 있으면, 저장된 refreshToken을 발급해줍니다.
- accessToken이 만료되어 있다면, 해당 유저의 토큰을 검증한 후 새로운 accessToken을 발급 과정을 거칩니다.
- refreshToken으로 새로운 accessToken을 발급할 수 있습니다.
JWT 라이브러리 의존성 추가 및 보안 설정
// build.gradle
dependencies {
...
/* jwt */
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
}
토큰 탈취 예방
access Token의 만료기간을 60분,
refresh Token의 만료기간을 14분으로 지정해주었습니다.
이와 같이 설정한 이유는, 너무 길면 토큰이 탈취당했을 때 서버의 인증을 다룰 때의 보안 취약점이 증가하고
너무 짧으면 사용자 경험이 불편해질 수 있기 때문입니다.
// application.yml
security:
jwt:
token:
secret-key: [시크릿 키]
access:
expire-length: 3600000
refresh:
expire-length: 1210000000
JWTProvider Bean 등록
- JWT Token Provider를 Spring 빈으로 등록해줍니다.
- 토큰application.yml에 적어놓은 token 정보(key, expire-length)를 가져와 등록해줍니다.
- 만료 시간 같은 경우 아직까지 accesToken을 통해 토큰 생성이 먼저라고 판단했기 때문에 refreshToken 보다 accesToken을 통해 발급에 우선 집중하자 생각했습니다.
JWT 생성
- 토큰의 식별 정보(subject), 토큰의 만료시간(Expiration), 서명(signWith), 직렬화(compact)를 진행합니다.
토큰에 왜 서명을 할까??
서버에서 토큰을 발급해줄 때, 토큰의 내용이 변경되지 않았음(데이터 무결성)과 토큰이 발급자에 의해서만 생성되었음(안정성)을 보증시키기 위해서입니다! 쉽게 말해서, 토큰에 서명함으로써 무결성과 안정성을 제공할 수 있는 것입니다!
비밀 키(key)를 따로 두어 서명을 생성&검증하는 데 사용했고, HS256 알고리즘을 활용해 보안성을 강화시켰습니다.
JWT Payload(사용자 식별정보) 검출
어떤 사용자가 요청을 보냈는지를 확인하기 위해 해당 JWT의 payload를 가져와 줍니다.
- JWT 데이터의 무결성과 안전성을 검증하는 데에 사용했던 기존 서버의 비밀 키를 통해 JWT에 접근(setSigningKey)합니다.
- 서명된 JWT를 파싱(parseClaimsJws)시켜, 어떤 사용자가 데이터 요청을 보냈는지(subject)를 반환해주었습니다.
JWT 데이터 유효성 검증
- payload를 가져올 때와 마찬가지로, 파싱 후 해당 서버에서 서명한 토큰(setSigningKey)인지 판단합니다.
- 서명된 JWT를 파싱하고 서명을 검증(parseClaimsJws)합니다.
- 이후, 토큰 데이터의 보안성을 위해 해당 토큰의 만료시간이 현재 시간보다 이전인지 확인했습니다.
- 만약, JWT 파싱을 할 때나 별도 IllegalArgumentException이 발생했을 경우에는 그에 맞는 에러를 반환하도록 해주었습니다.
마무리
위와 같이 JWT를 선택하고, 생성하는 로직을 설명해드렸으나
그럼에도 불구하고, 모든 상황에서는 적합하다고는 단정지을 수 없습니다.
예를 들어,
- payload의 정보가 많아진다면 네트워크 사용량이 증가해버립니다. 이는 곧 토큰의 크기가 커짐을 의미할건데, 만약 많은 사용자가 몰리는 대규모 서비스에서는 토큰을 검증하고 파싱할 때 오히려 더 인증 처리 시간 지연이 발생할 수 있습니다. 혹시나 만약, 이렇게 된다면 서버의 스케일링 측면을 고려하지 않을 수 없을 것입니다.
- 사용자가 해킹당했음을 본인의 계정이 해킹당했다고 의심할 때, 현재 현재 로그인한 회원이 어떤 컴퓨터로 접속했는지까지는 파악할 수 없습니다. stateless 를 띄어 한 번 발행되면 서버가 해당 토큰을 추적하지 못하기 때문입니다..
- 서버 단에서 상태를 관리하지 않기 때문에 강제 로그아웃을 시킬수는 없습니다.
- 이미 해킹된 토큰의 경우에도 stateless 때문에 블랙리스트에 올려 완벽히 차단하지 않는 이상은, 이에 즉각적인 대응 하기 어렵다 예상합니다.
결국, 여기에서 전달드리고 싶은 점은 특정 기술을 사용할 때에는 트레이드 오프를 고려하는 것이 무엇보다 중요하다는 것입니다.
잘못된 부분이 있거나 피드백이 있으시다면 꼭 언제든지 작성 부탁드립니다! 좋은 하루 되시길 바랍니다!!
Reference
http://wiki.hash.kr/index.php/SHA256
https://medium.com/@ff.primrose/all-about-session-and-jwt-b2836f5e5f08
https://maily.so/grabnews/posts/ecbe33
https://tecoble.techcourse.co.kr/post/2021-11-07-load-balancing/
'Project > 너도나도' 카테고리의 다른 글
[너도나도] EP 4. Layered Architecture를 선택한 이유 (0) | 2023.08.15 |
---|---|
[너도나도] EP 3. 카테고리 데이터 (0) | 2023.08.13 |
[너도나도] EP 2. 도메인 & 엔티티 설계 (0) | 2023.08.13 |
[너도나도] EP 1. 개발 계획 (0) | 2023.08.11 |