기존 JWT를 사용한 프로젝트를 진행할 때 Redis를 활용한다는 것은 알고 있었지만 쓸 줄 몰랐기에 DB를 통해 처리하는 방법으로 마무리를 했었다.
이번에 Redis를 학습하면서 이 프로젝트를 리펙토링하고자 리펙토링 계획을 세우고 있는데 고려해야 하는 사항이 많이 발생해서 그 내용을 정리한다.
고민에 대한 정리글이기 때문에 문제 해결에 대한 정답이나 명확한 방법을 찾은것은 아니고 개인적인 의견이니 보시고 다른 의견이나 피드백은 환영합니다.
일단 현재 구조를 먼저 정리.
AccessToken과 RefreshToken 두 가지를 생성해 처리 중.
AccessToken의 Claim으로는 userId를 갖고 있으며 RefreshToken은 DB 조회를 위해 rtIndex라는 난수를 Claim으로 갖고 있음.
AccessToken이 만료되어 재발급 받아야 하는 경우 RefreshToken 역시 재발급 해 갱신하는 RefreshToken Rotation 방식으로 처리하는 중.
목적은 RDB가 아닌 Redis로 토큰을 관리하는 것이 목적이며, 여러 디바이스에서의 로그인을 허용할 수 있도록 하는 것이 목표.
고민 및 문제점
Redis로 토큰을 관리하는 것은 Redis를 어떻게 사용해야 하는지만 배우면 되는 부분이라 문제가 되지 않았다.
문제는 RDB처럼 여러 필드를 통해 저장하지 않고 K:V 형태로 어떻게 관리를 할 것이며,
다중 로그인에 대한 관리 및 탈취에 대한 대비는 어떻게 할 것인가였다.
몇일을 JWT 다중 로그인에 대해 알아봤지만 마땅한 답을 얻기 어려웠다.
그러다 팁을 얻을 수 있는 블로그 포스팅을 발견했다.
https://junior-datalist.tistory.com/352
Refresh Token Rotation 과 Redis로 토큰 탈취 시나리오 대응
I. 서론 JWT와 Session 비교 및 JWT의 장점 소개 II. 본론 Access Token과 Refresh Token의 도입 이유 Refresh Token 은 어떻게 Access Token의 재발급을 도와주는 걸까? Refresh Token Rotation Redis 저장 방식 변경 III. 결론
junior-datalist.tistory.com
다중 로그인을 처리하신 것은 아니지만 생각하지 못했던 부분을 잡아 낼 수 있었다.
생각지 못했던 탈취당한 토큰으로 사용자보다 먼저 재발급을 받게 되는 경우까지 고려한 글이었다.
탈취자가 사용자보다 먼저 재발급을 받게 된다면 사용자는 정상적인 재발급을 받을 수 없게 되고, 로그아웃 처리가 되는 형태가 되어버린다.
그럼 탈취자는 재발급 받은 토큰으로 계속 사용이 가능하게 되며 사용자는 재로그인을 하게 되면서 한 아이디에 토큰이 두개가 되는 형태가 될 수 있다.
이걸 방지하기 위해 DB에 RefreshToken을 저장하는 것이고 사용자가 요청을 보냈을 때 자신의 아이디로 이미 RefreshToken이 존재한다면 탈취되었다고 판단하고 해당 데이터를 삭제하는 방법으로 탈취자의 사용을 막을 수 있게 된다.
이 포스팅을 보고 각 토큰의 Redis 저장 형태를 잡을 수 있었다.
rtuserId : RefreshTokenValue, atuserId : AccessTokenValue
처음에는 RefreshTokenValue : AccessTokenValue, AccessTokenValue : userId 형태로 처리하려고 했다.
다중 로그인을 처리하고자 하다보니 중복되지 않은 키값으로 각 토큰의 값을 key로 갖는것이 좋겠다고 생각했기 때문이다.
그리고 현재 구현되어있는 구조도 그런 구조로 되어있다.
RefreshToken의 Claim인 rtIndex가 기본키로 되어있고 RefreshTokenValue와 userId를 담고 있다.
이 구조에서도 이에 대한 대비가 부족하다고 이번에 느꼈다.
RefreshToken에서 Claim을 꺼내더라도 userId는 찾을 수 없고, DB 조회를 통해서만 알 수 있었다.
그럼 이미 갱신되어버린 이전 값으로는 사용자의 아이디도 찾을 수 없기 때문에 토큰 존재여부를 확인할 수 없다.
그래서 해당 블로그에서는 userId를 키값으로 갖는 구조를 얘기한다.
userId : RefreshTokenValue 이 구조라면 key값은 유지되고 value값만 갱신되기 때문에 value와 서버에서 받은 RefreshTokenValue가 다르다면 탈취한것으로 판단할 수 있다는 것이다.
그래서 RefreshToken의 Claim 역시 사용자 아이디로 수정하기로 했다.
그로인해 서버에서 받은 RefreshToken의 claim으로 key값을 알 수 있게 되고, value를 비교할 수 있게 된다.
그럼 탈취에 대한 대비는 어느정도 되었으니 다음 문제다.
'key가 userId 구조라면 다중 디바이스 로그인 처리에서 중복될텐데 이걸 어떻게 처리할 것인가?'
이 문제 역시 다양한 방안을 고민해봤다.
처음에는 기기 정보를 얻을 수 있으면 해결되지 않나 싶었다.
Mobile, Tablet, else 형태로 구분을 하는 방법은 있었다.
하지만, 각 디바이스별 앱인지, 웹앱인지를 또 구분해야 하고 그에 따른 차이도 필요했다.
그럼 디바이스 식별자 정보로 key를 생성하는것도 조금 애매해졌다.
https://wildeveloperetrain.tistory.com/297
JWT 인증 단점과 중복 로그인 관련 해결 방안
JWT 인증 기능의 단점과 문제점 그리고 중복 로그인 문제 해당 포스팅은 'jwt 인증 기능을 사용하면서 느꼈던 단점과 발생했던 문제점, 그리고 중복 로그인에 대한 나름의 해결 방안'을 정리한 것
wildeveloperetrain.tistory.com
이 문제는 여기서 답을 찾을 수 있었다.
로그인시 고유값을 하나 만들어서 클라이언트에 전달해주는 방법이다.
이 방법으로 UUID를 통해 임시로 tno라는 변수를 만들고 이걸 클라이언트에게 전달하는 방법을 생각했다.
그럼 디바이스별, 브라우저별로 각각 tno가 모두 다를것이기 때문에 key가 중복되는 일을 없앨 수 있다고 생각했다.
그래서 구조를 이렇게 생각했다.
"rt" + tno + userId : RefreshTokenValue, "at" + tno + userId : AccessTokenValue
그럼 모바일 기준 앱 환경에서 로그인 할때의 tno와 크롬이나 브라우저 애플리케이션에서 로그인할때의 tno가 다를 것이고 다중 로그인을 성공적으로 해결할 수 있다.
하지만 여기서 고려해야할 사항이 하나 더 있다.
tno는 사용자가 로그인을 할 때 마다 생성해서는 안된다.
처음에는 tno를 로그인 요청 프로세스 사이에 끼워서 매번 생성하고자 했다.
하지만 그렇게 처리하면 첫번째 고려사항인 탈취 대비에 대해 문제가 발생한다.
coco라는 아이디의 유저가 처음 로그인을 하고 asdf 라는 tno가 생성되었다고 치자.
그럼 rfasdfcoco : RefreshTokenValue, atasdfcoco : AccessTokenValue 이렇게 redis에 저장될 것이다.
여기서. 탈취가 되었다고 가정하자.
그럼 탈취자는 rfasdfcoco의 value값을 재발급 받게 될 것이다.
이때 사용자가 다시 접근한다면 동일한 tno를 갖고 있기 때문에 RefreshTokenValue가 다르다는 것을 서버가 알게되고 Redis에서 데이터를 삭제하게 될 것이다.
하지만, 만약 사용자가 RefreshToken의 만료기간 이상의 기간동안 접속하지 않는다면?
사용자는 RefreshToken 쿠키가 만료되어 토큰 정보를 전혀 갖고 있지 않게 될 것이다.
그럼 재 로그인을 해야될 것이고 그때 새로운 tno를 생성해 로그인 처리를 한다면
rfqwercoco : RefreshTokenValue 라는 데이터가 Redis에 담기게 될 것이고, 탈취자는 아무런 지장없이 rfasdfcoco 데이터를 통해 계속해서 기능 사용이 가능할 것이다.
이걸 방지하기 위해서는 최초 로그인시에 발급된 tno를 디바이스에서 계속 보관하도록 해야 하며, 만료기간이 없어야 한다.
그리고 재 로그인시에는 최초 발급된 tno를 통해 처리하도록 해야 중복되는 키값을 확인할 수 있고 서버가 탈취에 대한 대비를 할 수 있게 된다.
그럼 이제 아직 해결하지 못한 문제 하나는
이 tno를 어디다 저장할 것인가 이다.
쿠키의 경우 브라우저 설정에서 쿠키 삭제가 가능하다.
이걸 막을 수 있는 방법이 있는지는 아직 잘 모르겠고, 설정중에 브라우저 종료시마다 쿠키를 삭제하는 설정도 있었던 기억이 있다.
그럼 tno가 쿠키에 담기는 경우 이런 불상사가 일어날 수 있다.
그럼 localStorage에 저장해야 하나? 라는 생각을 했지만
localStorage는 아무래도 js에서 접근하기가 수월해 이것도 애매하다.
이 문제는.....
어떻게 해결할지 아직 답이 없다..
정리
1. Redis에 RefreshToken만 담는 것 보다 AccessToken까지 담는것이 재발급 요청에서의 토큰 탈취에 대비하기 더 좋다.
재발급 요청이 왔을 때 AccessToken의 만료기간을 체크해 아직 만료기간이 남아있다면 탈취된 토큰이라고 볼 수 있기 때문이다.
2. 토큰 탈취에 대비해 key값은 사용자 아이디 같은 사용자 고유의 정보가 포함되는 것이 좋다.
탈취자가 사용자보다 먼저 재발급을 받는 경우에도 토큰을 수월하게 비교할 수 있기 때문이다.
3. 다중 로그인을 허용하기 위해서는 각 디바이스별, 각 브라우저별 고유값을 최초 로그인시에 부여하고 삭제되지 않도록 해 관리하는것이 좋다.
고유값을 로그인마다 재 생성해 반환한다면 이전 고유값의 데이터와 토큰값이 탈취당한 경우 잡아낼 수 없다.
단, 이 고유값을 클라이언트에서 어디에 어떻게 저장하고 관리하도록 할지에 대한 고민이 남아있다.
'Project&문제해결' 카테고리의 다른 글
Kotlin build.gradle.kts 빨간 밑줄 문제 해결 (0) | 2024.08.06 |
---|---|
React 권한 페이지 접근 렌더링 문제 해결 (0) | 2024.04.12 |
JDBCTemplate 동적 쿼리 문제 해결 (0) | 2023.10.20 |
Servlet&JSP에서 Ajax Response (0) | 2023.10.20 |
@Transactional의 rollback처리 문제 해결 (0) | 2023.09.29 |