📌개요
프로젝트를 진행하면서 로그인 유지 기법 으로는 크게 세션 VS 토큰으로 비교 대상군이 되고는 합니다.
이 중에서 저는 세션 방식과 토큰 방식 중 왜 토큰 방식을 택했으며,
토큰 방식 중 JWT를 선택하게 되면서 얻었던 보안적인 고찰에 대해서 적어보고자 합니다.
해당 포스팅은 JWT에 대한 설명 보다는 JWT 에 보안을 올려왔던 과정에 대한 글 인점을 참고해주세요!
🔍프로젝트에서 토큰 방식을 택했던 이유
저는 서버를 단일 서버가 아닌 쿠버네티스 환경에 올렸기 때문에,
쿠버네티스 환경을 통해서 서버의 개수를 자유롭게 늘리고 줄일 수 있는 자율성을 가질 수 있었습니다.
하지만 세션 방식의 가장 큰 단점이라고 한다면 아래와 같을 것 입니다.

세션 정보의 가장 큰 단점은
회원이 서버1 에서 로그인 요청을 하고 세션 정보를 공유하게 되었지만 만일 로드밸런서에 의해서 서버2 에게 다음 요청 작업을 부탁하게 되었다면 서버2는 동기화 과정 없이는 세션 정보를 알 수가 없어서 다시 로그인을 해야하는 상황이 발생하게 됩니다.
이에 따라 서버가 세션 정보를 직접 관리하여 로그인을 유지시키는 방법 보다는
클라이언트 측의 토큰에 따라 검증 과정만 거쳐 유지 시키는 것이 나을 것 같다 라는 판단을 하게 되어 아래와 같이 토큰 방식을 채택하였습니다.

토큰의 장점이라고 한다면, 세션과 달리 서버간의 동기화 과정이나, 고정된 서버로 보내는 추가 작업을 해주지 않고, 직접 전달받은 토큰에 의해서만 판단하면 될 것 입니다.
하지만 서버가 정보를 관리하지 않는다. 라는 것은 편할 수도 있겠지만, 양날의 검 처럼 굉장히 치명적인 단점을 가지고 있습니다.
문제는 토큰이 탈취 될 경우에 발생합니다.
🥊 서버는 정상적인 유저의 토큰인지, 해커에게 탈취된 토큰인지 알 수가 없다.

실제로 토큰 방식에서 가장 큰 문제점으로 삼는 것은 바로 "보안성"일 것 입니다.
JWT 특성상 암호화를 건다고는 하지만, 서버의 입장문으로서 작용되는 토큰은 결코 정상 토큰인지, 탈취된 토큰인지 알 수 없습니다.
따라서 반드시 해당 상황에 대처하기 위한 추가적인 작업들이 필요 할 것 입니다.
🔨 내가 선택한 보안성 강화 방법

저는 아래와 같은 3가지 방법으로 보안성을 강화시켜보고자 노력하였습니다.
🔨 1차적 보안 : Http Only, Secure 설정
쿠키로 전달하는 과정 중 Http Only , Secure 의 설정을 추가적으로 걸어줌으로서, 일련의 보안성을 높일 수 있었습니다.

해당 옵션들의 효과는 아주 간단히 설명드리자면 다음과 같습니다.
1. Http Only : 해당 옵션은 클라이언트단에서 javascript 로 쿠키를 조작할 수 없습니다. 따라서 불법적인 접근을 조금이나마 막을 수 있을 것 입니다.
2. Secure : 해당 옵션은 HTTP 가 아닌 HTTPS 에서의 환경에서만 쿠키를 주고받을 수 있게 설정합니다.
즉, HTTP 환경에서는 쿠키가 절대로 세팅 될 수 없기에 SSL 이라는 보안의 힘을 업어갈 수 있을 것 입니다.
하지만, 만일 해커가 1차적인 보안을 뚫고 Access Token 을 탈취 할 경우에는 어떨까요?
1차적인 보안만을 가지고 보안성을 가지기에는 다소 위험성이 높습니다.
따라서 저는, 2차적인 보안을 위해 Refresh Token 전략을 도입하게 됩니다.
🔨 2차적 보안 : Refresh Token 추가 전달

저는 만일 1차적인 보안을 뚫고 탈취가 되었을 때에도 대처하기 위해서 2차적인 보안 방법으로 Refresh Token 을 함께 보내는 전략을 채택하였습니다.
Refresh Token에 의해서 재발급이 가능하기에, AccessToken의 만료시간을 아주 짧게 설정할 수 있었고,
아래와 같이 Access Token 이 탈취되더라도 오직 30분이라는 시간만 주어지기에 탈취 되어도 위험성을 낮출 수 있었습니다.

하지만, Refresh Token을 구현하신 분들 중, 혹시라도 이런 생각을 해보신 적이 있으신가요?
엥? 만약에 Access Token 과 Refresh Token이 둘 다 탈취되면 큰일 나는거 아니야?
❗❗Refresh Token을 전달 할 경우에는 반드시 추가적인 작업이 필요하다!

저도 위와 같은 고민을 하게 되었고, 실제로, Refresh Token만 구현하고 아무런 대처를 하지 않는다면, 그저 해커가 더욱 신나게 날 뛸 수 있도록 도와주는 상황이 발생할 수도 있습니다.
따라서 저는 Refresh Token에 대해서도 보안을 강화해주기로 결심하게 됩니다.
💪 Refresh Token 보안 전략 2가지 ( RTR / BlackList )

👌첫 번째 전략 : Refresh Token Rotation 이라고 칭하는 RTR 전략
해당 전략을 간략히 설명드리자면, 기존에는 AT(Access Token)가 만료시 AT 만 재갱신을 시켜주었었다면, 이제는 RT(Refresh TOken) 또한 재갱신을 시키는 방법입니다!
해당 방법의 효과는 다음과 같습니다.
RT 가 재갱신이 될 때 해당 토큰을 DB에 저장을 하고, 이전에 가지고 있던 RT 는 쓸모가 없어지는 것 입니다. 즉, 해커가 가지고 있는 RT 로는 이제 더이상 AT의 재갱신이 불가능합니다. ( 1회용성 RT 라고도 불리웁니다. )
if(jwtUtil.isExpired(accessToken)){
String reIssuedAccessToken = refreshTokenService.reIssueAccessToken(refreshToken);
---검증 과정 중략---
// AccessToken 재갱신
token = reIssuedAccessToken;
Cookie cookie = new Cookie("JToken", token);
cookie.setHttpOnly(true);
cookie.setSecure(true);
cookie.setPath("/");
response.addCookie(cookie);
// RefreshToken 재갱신
refreshToken = refreshTokenService.reIssueRefreshToken(refreshToken); //RTR 적용
Cookie reissuedRefreshToken = new Cookie("RefreshToken", refreshToken);
reissuedRefreshToken.setHttpOnly(true);
reissuedRefreshToken.setSecure(true);
reissuedRefreshToken.setPath("/");
response.addCookie(reissuedRefreshToken);
}
하지만 해당 RTR 전략도 분명 허점이 존재합니다.
만일, 사용자가 가장 마지막에 사용한 RT 를 해커가 확보하게 된다면 어떻게 될까요?
가장 마지막에 사용 된 RT 를 해커가 습득한다면, 유저가 다시 로그인이나 만료를 맛보지 않는이상, 해당 무한 갱신의 역할은 해커가 가지게 됩니다.
따라서 저는 마지막 전략을 세우게 되었습니다.
👌 두 번째 전략 : 로그아웃 BlackList 전략
BlackList 전략은 로그아웃시에, Database 에 아예 블랙리스트로 등록해버리는 전략입니다.

해당 효과는 다음과 같습니다.
유저는 본인의 웹사이트 방문이 모두 끝나면, 로그아웃 요청을 보내게 됩니다. 로그아웃 요청이 들어오게 되면,
AT 와 RT 모두 Database 에 블랙리스트 토큰을 저장시킵니다.
이말은 즉슨, 이제 해당 토큰은 블랙리스트 이기 때문에, 이후에 들어오는 같은 토큰은 모두 탈취된 토큰으로 간주하겠다. 를 뜻합니다. 이후에 토큰이 들어오게 되면 해당 클라이언트가 가지고 있는 쿠키를 그 즉시 삭제시킬 수 있게됩니다.
JWT 확인 필터
// 1. 쿠키를 통한 요청 받기
if(request.getCookies() != null){
for(Cookie cookie : request.getCookies()){
if(cookie.getName().equals("JToken")){
accessToken = cookie.getValue();
}
if (cookie.getName().equals("RefreshToken")) {
refreshToken = cookie.getValue();
}
}
}
// 2. 블랙리스트에 등록되어 있으면 불법 토큰으로 판단
if(blackListTokenService.checkBlackList(accessToken,refreshToken)){
System.out.println("블랙리스트 토큰");
// 불법 쿠키 삭제
Cookie deleteAccessTokenCookie = new Cookie("JToken", null);
deleteAccessTokenCookie.setMaxAge(0);
deleteAccessTokenCookie.setPath("/");
response.addCookie(deleteAccessTokenCookie);
Cookie deleteRefreshTokenCookie = new Cookie("RefreshToken", null);
deleteRefreshTokenCookie.setMaxAge(0);
deleteRefreshTokenCookie.setPath("/");
response.addCookie(deleteRefreshTokenCookie);
---중략---
}
BlackList 탐색 서비스
public class BlackListTokenService {
private final BlackListTokenRepository blackListTokenRepository;
public boolean checkBlackList(String accessToken, String refreshToken){
boolean existAccess = false;
boolean existRefresh = false;
if(accessToken != null){
existAccess = blackListTokenRepository.existsByToken(accessToken);
}
if(refreshToken != null){
existRefresh = blackListTokenRepository.existsByToken(refreshToken);
}
if(existAccess || existRefresh){
return true;
}
return false;
}
}
이로써 다양한 보안과정을 통해서 Token에 보안성을 강화 시켜줄 수 있었습니다.
하지만 하면 할수록 이럴꺼면 세션방식을 ㅆ
😂 RDB 에 토큰을 저장시켰을 때의 문제점 발생
그러나 블랙리스트의 가장 큰 단점은 로그아웃한 모든 토큰을 db에 저장시킨다는 점입니다.
보안을 높이기 위해서, 토큰의 정보를 DB가 관리하게 되면서, DB에 대해서 부하가 굉장히 늘어나게 되는 것을 본 프로젝트를 진행하며 알게 되었습니다.


짧은 AT의 만료시간 마다, blackList 조회는 일어나고,
매 로그아웃 요청마다 DB안에 2개의 데이터도 고정적으로 쌓이게 되어 주기적으로 삭제 시켜주는 작업까지 필요하게 되었습니다.

서비스를 이용하는 유저 중 로그아웃 횟수가 571번 일 때만 하더라도 DB 안에 순식간에 1142개의 토큰이 삭제되지 않고 계속 저장되는 것을 볼 수 있습니다.
만약 유저가 1만명, 10만명 이라면 로그아웃 요청 횟수마다 DB에 저장되는 토큰은 상상 이상일 것입니다.
이에 따라 Redis 도입을 결정하였습니다.
👊 최종, Redis 도입
Redis는 In-memory 방식이므로 디스크가 아닌 메모리에 저장되기 때문에 데이터를 조회할때 속도가 빠르고,
TTL 기능을 기본적으로 제공 해주기 때문에, 만료된 토큰을 자동 삭제하여 DB의 부하를 줄일 수 있어 성능 개선에 적합하다고 판단하였습니다.
RDB에 있는 정보를 Redis로 옮기게 되면서 성능적인 개선은 다음과 같습니다.
- DB에서 조회 하지않고, 사전에 In-memory Database 에서 값을 조회하기에, 조회 속도가 빠르며 DB에 대한 부담도 줄일 수 있었다.
- TTL 기간을 RefreshToken 의 만료시간과 동일하게 설정해주어, 개별 토큰마다 자동삭제가 가능해졌다.


마무리
물론 아직까지, 회원이 사이트 이용을 마치고, 로그아웃 요청을 진행하지 않으면 대처하기는 다소 어려움이 있다라는 숙제는 남아있으나, 이러한 일련의 과정을 통해서 본 서비스에서 토큰에 대한 보안성을 높일 수 있었습니다. 💪🙇♂️
이상 해당 포스팅을 읽어주셔서 감사합니다 😀
'[TIL]' 카테고리의 다른 글
| [ TIL ] RECOVER_YOUR_DATA : RDS 해킹 일지 (1) | 2024.11.13 |
|---|---|
| Kubernetes 분산 서버에 의한 Oauth2 소셜 인증 실패 및 극복 방법 (sticky session service 추가) (2) | 2024.10.13 |
| ELK 에 대해서 알아보자! ( ElasticSearch - Logstash - Kibana ) (2) | 2024.09.22 |
| Cookie 에 Http Only 와 secure 를 적용시켜보자! [ Spring boot + Vue 3.x] (0) | 2024.08.26 |
| [TIL] 도메인 적용 방법 + HTTP에서 HTTPS로 전환 방법 [Nginx, 내도메인한국, Zerossl] (1) | 2024.08.16 |