본문 바로가기
Programming/Study

JWT 이대로 괜찮은가?

by JKROH 2024. 1. 2.
반응형

 인증 / 인가에 JWT를 사용하여 구현하는 경우가 상당히 많다. JWT를 사용하는 이유는 Stateful해서 서버에 부하가 걸릴 수 있는 세션과 달리 Stateless하며, 중요한 식별값(아이디, 비밀번호 등)이 그대로 넘어가는 쿠키에 비해 안전하다는 것이다.

 

 그런데 순수하게 Stateless한 JWT를 사용하는 것이 과연 옳은가? 이 부분에 대해서 의구심이 든다. Stateless하다는 것은 달리 말하면 서버가 해당 토큰에 대해 모른다는 것이다. 즉, 토큰을 사용한 공격에는 취약한 점이 있다. 동시에, 로그아웃을 명확히 구현해내기 굉장히 어렵다.

 

 가장 널리 알려진 경우가 다중 로그인일 것이다. 순수 JWT를 사용하면 한 계정으로 여러 번 로그인 하는 것을 막을 수 없다. 그렇다고 이미 로그인한 사용자의 정보를 어딘가에 저장하면 그건 이미 Stateful한 상태이며, 그럴거면 그냥 세션을 쓰는게 낫다.

 

 또한 순수 JWT를 사용하면 로그아웃 자체를 구현할 수가 없다. 정확히는 구현은 할 수 있지만 너무 안전하지 않다. 이건 잠시 후에 이야기하기로 하자. 로그아웃은 어떤 사용자가 로그인 했다는 것을 전제로 해야 가능한 유즈케이스다. 그런데 JWT를 사용하면 이게 내가 발급한 JWT가 맞는지만 검증할 수 있을 뿐, 지금 이 사용자가 로그인 했는지는 알 길이 없다.

 

 로그아웃 요청이 오면 Security Context 자체를 clear해버리는 방법으로 JWT 로그아웃을 구현 할 수도 있다. 그러나 이 방법은 기본적으로 안전하지 않다고 생각한다. Security Context의 기본 규칙은 ThreadLocal한 Security Context를 사용하는 것이다. 하나의 쓰레드 당 하나의 Security Context가 존재하는데, Security Context에는 여러 사용자의 인증 정보가 존재한다. 그런데 한 사용자가 로그아웃 요청을 보냈다고 그 Context를 clear한다? 이건 말이 안된다.

 

 어찌저찌 로그아웃을 구현했다고 해보자. 그럼 이번에 들어온 토큰이 로그아웃 된 토큰인지는 또 어떻게 검증할 것인가? 요청이 들어올 때마다 데이터베이스를 찔러본다? 그럴거면 그냥 세션 쓰지... 몇몇 분들은 Redis를 사용해서 RefreshToken 또는 AccessToken을 Redis에 저장하고 거기서 검색해보시던데 그럴거면 그냥 세션 쓰지...

 

 사실 내가 그렇게 구현했다. Redis에 저장하고, 요청 들어올 때마다 Redis 찔러보고. 다 구현하고 나니 생각이 들더라. 이럴거면 그냥 세션 쓰지...

 

 그래서 JWT는 사용하면 안되나?? 그건 또 아닌 것 같다. 내가 생각하는 해결법은 일부 Stateful한 방식을 사용함에 더해 토큰을 강제로 만료시키는 것이다. 첫 번째 방법은 다중 로그인을 방지하기 위해, 두 번째 방법은 로그아웃을 구현하기 위해 생각한 해결 방안이다.

 

 먼저, 결국 다중 로그인을 막기 위해서는 어느 정도는 Stateful한 방식을 선택할 수밖에 없다. 여기에 Redis를 사용해보는 것이 어떨까하는 생각이 든다. 예를 들어, 로그인(인증) 요청이 들어오면 Redis에 해당 사용자가 저장되어 있는지를 검증하는 것이다. Redis Template에 해당 사용자의 VO, Access Token / Refresh Token, 만료시간 등을 저장해 인증 요청이 올 때만 Redis를 찔러서 해당 사용자가 인증 요청을 보냈는지를 검증한다. 로그인 상태가 아니면 Redis에 정보 저장하고 원래대로 인증 처리를 진행하고, 만약에 로그인이 되어있는 상태면 기존 토큰을 강제 만료 시키고 새로 발급한 토큰으로 갱신한다거나 아니면 로그인 로직 자체를 막는다던가. 아마 로그인 로직 자체를 막는 건 안할 것 같다. 말이 안되잖아, 내 아이디 해킹 됐는데 나는 못들어간다는게.

 

 아무튼 이 방법을 사용하면 인증에는 Stateful한 상태가 유지되지만, 인가에서는 Stateless한 상태를 유지할 수 있다. 여전히 우리는 토큰을 내가 발급한게 맞는지만 검증하면 된다. 물론 무수한 로그인의 요청이라는 공격도 있을 수 있겠지만, 완벽하게 모든걸 해결할 수 있는 방법은 없다.

 

 다음으로 토큰 강제 만료인데, Redis를 찌르지 않고 로그아웃을 확인하려면 결국 토큰을 강제로 만료시키는 방법밖에 없다. 물론 그게 어려워서 Redis를 쓰는 거긴 한데, 그럴거면 그냥 세션 쓰지... 관련해서 여러 글들을 찾아봤고, 한 분의 글에서 큰 힌트를 얻었다. 이 분의 글을 참조해서 로그아웃 로직에 대한 대대적인 리팩토링이 진행되지 않을까 싶다.

 

 문제는 위의 방법을 제대로 테스트하기 위해선 프론트엔드와의 협업이 반드시 필요하다는 것이다. 토큰 강제 만료가 통했는지, 아니면 클라이언트 단에서 강제로 로컬에 저장된 토큰 값을 지울 수 있는지를 확인해보고 싶은데... 문제는 어디서 프론트엔드 팀원을 구해야할지 모르겠다. 그냥 내가 짤까도 생각되고... 일단은 최대한 아는 커뮤니티 내에서 구해봐야겠다.

 

 이번에 로그아웃 로직을 짜면서 내가 너무 나이브하게 인증 / 인가에 접근하고 있었다고 생각됐다. 왜 토큰을 쓰는지도 제대로 이해하지 못 한 채로 막무가내 억지 구현만 했구나 싶기도 하고. 오늘은 구현은 짧게 끝내고 반드시 책을 좀 읽으리라 다짐하고 책상 앞에 앉았는데, 거의 4시간 정도를 일어나지도 못하고 검색하고 찾아보고 하느라 시간을 사용했다. 뭐 그만큼 얻어간게 있으니 다행이긴 하다만.

 

 아무튼 정리하자면 다음과 같다. JWT를 사용해서 순수하게 Stateless하게 인증 / 인가 처리를 구현하는 것은 (거의) 불가능하다. 내가 모르는 영역에서 해결한 방안이 있을 수도 있겠지만, 아마 결국에는 일부는 Stateful하지 않을까 싶다. 또한 JWT를 사용해서 서버 레벨에서 모든 처리를 하는 것은 마찬가지로 어렵다. 결국 클라이언트의 도움이 필요한 부분이다. 

반응형

댓글