JWT 인증 기능의 단점과 문제점 그리고 중복 로그인 문제
해당 포스팅은 'jwt 인증 기능을 사용하면서 느꼈던 단점과 발생했던 문제점, 그리고 중복 로그인에 대한 나름의 해결 방안'을 정리한 것입니다.
사실 '해결'이라기보다는 '타협'에 더 가까운 것 같으며, 실제 코드보다 이론적으로 풀어서 쓴 내용이 많다는 점 참고 부탁드리겠습니다.
1. JWT 인증 사용 이유
클라우드 기반의 MSA 환경과 RESTful API가 증가하는 등의 영향으로 인해 기존에 사용되던 '세션(Session)' 기반 인증 대신 Stateless라는 특징을 가진 'JWT(JSON Web Token)' 인증 방식이 많이 활용되고 있는데요.
세션 기반 인증의 경우 서버에서 세션에 대한 인증 정보를 가지고 있어야 하지만, JWT는 토큰 자체에 인증 정보를 가지고 있기 때문에 서버는 토큰에 대한 검증만 해주면 된다(세션에 대한 관리가 필요하지 않다)는 등의 장점이 있습니다.
2. 문제점
앞서 말한 것처럼 jwt는 상태를 저장하지 않는다는(stateless) 것이 가장 큰 특징인데요.
이는 한번 만들어진 토큰에 대해서는 제어가 불가능하다는 것으로, 발급된 토큰에 대한 삭제가 불가능하기 때문에 토큰 발급 시 만료 기간을 주는 방식이 적용됩니다.
하지만 만료 시간을 준다고 해도 토큰을 탈취당한 경우, 탈취자는 만료 시간이 끝날 때까지 해당 토큰을 자유롭게 사용할 수 있으며, 서버에서는 거기에 대한 대처를 할 수 없다는 문제가 있는데요.
3. RefreshToken
토큰 탈취에 대한 문제를 보완하기 위해 인증에 사용되는 토큰(Access Token)에 대해 30분 정도의 짧은 만료 시간을 부여하고, 대신 인증 토큰을 재발급할 수 있는 만료 기간이 1주일에서 2주일 정도로 긴 재발급 토큰(Refresh Token)을 함께 발급하는 방식이 사용되고 있습니다.
여기서는 refreshToken이 탈취되면 어떻게 하느냐는 문제가 다시 발생하게 되는데요.
이 문제를 해결하기 위해 'RTR(Refresh Token Rotation)', refresh token을 한 번만 사용할 수 있도록 하는 방법이 추가로 적용됩니다.
(refreshToken을 사용하여 accessToken을 재발급받을 때 refreshToken도 함께 재발급하는 것이며, 이 경우 재사용을 막기 위해 리프레시 토큰은 서버단에서 관리가 되어야 합니다.)
하지만 이 방식 역시도 accessToken 또는 재발급 요청을 하기 전의 refreshToken이 탈취된다면 서버에서 대처를 할 수 없게 됩니다.
4. 결국 서버에 인증 정보 저장
위의 RTR 방식을 적용하기 위해서는 결국 서버에서 인증 정보를 저장할 수밖에 없는데요.
추가로 로그아웃 기능에서 로그아웃 된 사용자의 인증 토큰(Access Token)에 대한 다음 접근을 막기 위해서도 결국 서버단에서 인증 정보를 관리해야 합니다.
***
결국 jwt 인증 기능은 탈취에 대한 완전한 해결 방법도 없을뿐더러 서버단에서 인증 정보에 대한 관리도 해야 하는 상황이 되었습니다.
(탈취에 대한 해결 방법이 없는 것은 jwt의 stateless 한 특징으로 인해 피할 수 없는 부분인 것 같습니다.)
5. 사용 중인 방법
@Builder
@Getter
@AllArgsConstructor
@NoArgsConstructor
@RedisHash(value = "token", timeToLive = 604800)
public class AuthToken {
@Id
private String id;
private String ip;
@Indexed
private String accessToken;
@Indexed
private String refreshToken;
}
(redis에 저장될 AuthToken 예시)
위 내용을 기반으로 JWT 인증 기능을 구현하기 위해 인메모리 데이터 저장소인 Redis를 함께 적용했는데요.
다음과 같이 '사용자 정보' + '발급된 인증 토큰 정보'를 담은 인스턴스를 redis에 저장하는 방식을 사용했습니다.
로그인 요청이 오고 id, password가 일치하여 토큰을 발급할 때, redis에 저장된 정보를 통해 해당 id로 기존에 발급된 토큰이 있는지 확인 후, 발급된 토큰이 있다면 기존 정보에 새로 발급된 토큰 정보를 update 하여 저장합니다.
로그아웃 시에는 요청이 들어온 accessToken을 가지고 redis에 저장된 인스턴스를 조회하여 삭제시킵니다.
대신 이렇게 적용했을 때 기본적인 api 요청 시 redis에 해당 accessToken이 있는지 여부를 확인해 주는 과정이 추가로 필요합니다.
(jwt 인증 이후 redis에 있는지 여부를 확인하는 과정이 이중으로 발생합니다.)
위 방식에 관련된 내용은 아래 포스팅을 참고할 수 있으며, 포스팅 내용에서 변경된 부분이 있기 때문에 전체적인 사용 방법 및 흐름만 함께 봐주시면 좋을 것 같습니다.
2023.03.12 - [Programming/Spring Boot] - (Spring Security + JWT) Refresh Token을 통한 토큰 재발급에 대해서
***
해당 방식의 문제점 중 하나는 redis에 저장되는 인스턴스에 대한 ttl(time to live)을 리프레시 토큰 기준으로 잡기 때문에 이미 만료된 액세스 토큰이 계속 함께 저장되어 있다는 것인데요.
따라서 accessToken, refreshToken을 따로 분리하여 저장하는 방법도 적용될 수 있습니다.
6. 중복 로그인에 대한 문제
위에 있는 방식을 적용한다면 로그인 시 사용자 id를 통해 기존에 발급된 토큰이 있는지 여부를 확인할 수 있기 때문에 중복 로그인을 막을 수 있게 됩니다.
하지만 문제는 중복 로그인 기능이 필요한 경우인데요. 쉽게 생각하면 중복 로그인을 가능하게 하기 위해서는 로그인 시 사용자 id로 이미 발급된 토큰이 있는지 체크하는 부분을 빼면 됩니다.
그러나 이렇게 되면 또 발생하는 문제는 로그인 요청이 올 때마다 새로운 인증 정보가 redis에 계속 저장이 된다는 것인데요.
redis에 저장하는 인스턴스는 로그아웃을 하지 않는 이상 설정된 ttl 동안 남아있기 때문에 어마어마한 양의 인증 정보가 저장될 수 있고, 결국 메모리 문제가 발생할 수 있습니다.
이런 상황을 해결하기 위해 적용할 수 있는 방법은 한 사용자에 대해 최대 중복 로그인이 가능한 수를 지정하거나, 중복 로그인이 가능한 기준을 두는 것인데요.
첫 번째, 사용자별로 최대 중복 로그인이 가능한 수를 지정하는 방법에서는, 만약 3개를 최대로 지정했다고 가정했을 때 3개가 로그인이 된 상태에서 추가로 로그인이 된 경우 기존의 인증 정보 3개 중에 어떤 것을 지워야 하느냐에 대한 기준이 필요하게 됩니다.
두 번째, 중복 로그인이 가능한 기준을 둔다는 것은 하나의 기기에서 로그인했을 때는 중복 발급이 아닌 갱신을 시키는 방법인데요.
이때 하나의 기기에서 로그인했다는 것을 어떻게 확인하느냐에 대한 문제점이 있으며, ip의 경우 기기가 다르더라도 공유기 등을 사용하는 경우 같은 ip로 요청이 올 수 있기 때문에 확인이 불가능합니다. 때문에 ip가 아닌 기기별 어떤 고유한 값이 필요하게 됩니다.
이 부분에 대해서는 회사 동료분께서 아이디어를 주셨는데요.
프론트 또는 백엔드에서 고유 값으로 쓸 수 있는 것을 만들어 최초 로그인한 클라이언트에게 넘겨주고, 클라이언트는 해당 값을 저장한 뒤 이후 로그인 시 해당 값을 함께 보내는 것입니다.
(login api의 요청 parameter를 id, password 외 추가로 하나 더 받아 내부적으로 처리합니다. 최초 로그인 시에는 없기 때문에 nullable로 받습니다.)
이렇게 했을 때 기기별 고유 값으로 쓸 수 있는 것이 생기기 때문에 인증 정보에 대한 중복 발급이 완전히 없어지지는 않더라도 어느 정도 줄일 수 있게 됩니다.
7. 마지막으로
먼저 해당 내용은 jwt 인증을 사용하는 하나의 방식일 뿐이라는 점 참고 부탁드리겠습니다.
저 역시 이렇게 적용하면서 과연 jwt를 이렇게까지 쓰는 게 맞을까 하는 생각이 들었는데요.
위 내용에 대해 궁금한 점이나 개선점, 지적해 주실 만한 부분은 댓글로 남겨주시면 감사드리겠습니다.
< 관련 자료 >
2021.08.12 - [Programming/Spring Boot] - JWT 토큰 기반 인증 시스템 (JSON Web Token)
2021.09.23 - [Programming/Spring Boot] - security + jwt + redis 로그인 기능 구현 (최종)
'Programming > Web' 카테고리의 다른 글
x-www-form-urlencoded 타입이란 (multipart/form-data와의 차이점) (0) | 2023.08.23 |
---|---|
GCP 방화벽 허용하는 방법(외부 접속 port 허용) (0) | 2023.08.21 |
스트랭글러 패턴(Strangler Pattern) 개념 정리 (0) | 2023.05.31 |
CDN(Content Delivery Network)이란? 콘텐츠 전송 네트워크 개념 정리 (0) | 2023.05.28 |
무료 ssl 인증서 적용하는 방법(Let's Encrypt) (0) | 2023.04.15 |