이번 게시글은 개발을 하는 도중에 Spring OAuth 2.0 을 jwt 방식으로 사용하면서 겪은 시행착오에 대해서 작성하려고 한다.
우선 사용자의 인증과 인가를 처리할 때 쿠키 방식, 세션 방식, JWT 토큰 방식의 차이점에 대해서 말해보겠다
쿠키 인증 방식은 쿠키에 사용자의 인증 정보를 담아서 인증하는 방식으로 클라이언트가 인증 정보를 관리한다
대략적인 흐름은
1. 클라이언트가 서버에 첫 로그인 인증 요청을 보내면, 서버에서 응답으로 쿠키에 사용자 인증 정보를 담아서 보낸다
2. 서버에서 응답한 쿠키를 클라이언트에서 저장하고, 인증/인가 요청 시마다 서버로 요청한다
흐름이 간단해보이는 것처럼 인증/인가 작업이 가장 쉽고 간단하다. 사용자의 인증 정보를 클라이언트가 관리하기 때문에 서버 부하가 적고, 인증 상태를 서버가 관리하지 않고 매번 클라이언트의 인증 정보를 담은 쿠키의 요청을 받을 때 처리하므로 Stateless 하다.
그러나 인증/인가 작업이 간단한 만큼 가장 치명적인 단점이 존재한다
CSRF
, XSS
공격등에 매우 취약하기 때문에 3가지의 인증/인가 방법 중에서 가장 기초적이고 사용하지 않는 방식이다
다음으로는 세션 인증 방식이다. 세션 인증 방식은 세션으로 사용자의 인증 정보를 관리하는 방식이다
대략적인 인증/인가 방식은 아래와 같다
SessionID
를 클라이언트에게 제공한다SessionId
로 서버에게 인증, 인가 요청을 해서 서버에서 SessionID
에 해당하는 인증 정보로 인증/인가를 처리한다쿠키 인증 방식과는 다르게 사용자의 인증 정보를 클라이언트가 아닌 서버가 관리한다. 그렇기 때문에 보안상 훨씬 안전하다 추가로 세션의 장점은
여기서 서버가 사용자 인증 정보를 관리하는 곳에 따라서도 장단점이 나뉜다.
크게 메모리, 하드디스크, DB에서 사용자의 인증 정보를 관리한다.
메모리 영역에서 사용자의 인증 정보를 관리하게 되면, 속도가 하드디스크나 DB에서 관리하는 것에 비해 빠르지만 사용자가 동시에 SessionId
로 인증 요청을 많이 보내게 되면 서버 메모리가 부족해지고, 서버를 껐다 켰을 때 Session 정보가 휘발된다는 치명적인 단점이 존재한다
하드디스크에서 관리하게되면 메모리에서보다는 속도가 느리지만 DB보다는 빠르다 그리고 발생하는 문제점은 메모리도 마찬가지지만
Scale-out
을 사용해서 서버가 여러 대 존재하여 로드 밸런싱을 사용할 때 한 사용자가 로그인 후 다음 인가 요청 시 로드 밸런싱으로 인해 사용자가 로그인한 서버가 아닌 다른 서버로 요청이 보내지면 다시 재로그인 필요하다.
한마디로 사용하는 서버가 여러개일때에는 인증 요청과 인가 요청 대상 서버가 달라지면 재로그인을 해야 한다는 것이다.
마지막으로 서버DB에서 사용자 인증 정보를 관리하는 방식이 있다 속도는 가장 느리지만 위에서 말한 단점들을 모두 해결할 수 있다 그렇기 때문에 세션으로 인증/인가를 진행을 해야한다면 서버 DB에 사용자 인증 정보를 관리하는 것이 가장 효율적이다.
단점은 로그인한 모든 유저의 인증 정보를 DB에 관리해야하므로 무겁고, 매 인증마다 DB를 거쳐 인증 정보를 Select 해야하므로 연결 비용이 클 수 있다.
마지막으로 JWT 토큰 방식에 대해서 설명하겠다. 인증/인가 흐름은 아래와 같다
1. 클라이언트가 서버에 첫 로그인 인증 요청을 보내면, 서버에서 응답으로 Token을 담아서 보낸다
2. 서버에서 응답한 Token을 클라이언트에서 저장하고, 인증/인가 요청 시마다 서버로 요청한다
이것만 들으면 기본적으로 클라이언트가 인증 정보를 관리하기 때문에 쿠키 인증 방식과 뭐가 다르지?라는 생각이 나는 처음에 들었다. 하지만 좀 더 알아보니 차이점은 쿠키가 아닌 JWT 토큰을 매개체로 인증한다는 것과 서버에서 토큰 검증
이 이루어지는 것이 쿠키에 비해서 보안적으로 안전하게 관리될 수 있는 차이점이다.
JWT는 사용자의 인증 정보와 서버의 SecretKey
로 이루어져 서버가 생성한 Token이기 때문에 요청으로 온 Token이 서버에서 발급한 Token인지 검증할 때 Token의 SecretKey가 다르다면 위조, 변조된 Token이라고 판단할 수 있다
세션 방식과 비교해서는 기본적으로 클라이언트가 Token을 관리하기 때문에 서버 부하가 비교적 적다. 단점은 보안이 세션 방식 보다는 취약하고 세션 방식의 여러 기능들을 사용할 수 없다는 단점이 존재했다.
나는 현재 프로젝트에서 JWT
토큰 방식을 사용하고 있다
그래서 내가 JWT 토큰을 사용하면서 겪었던 고민을 말해보겠다
그러기 전에 우선 Access Token
과 Refresh Token
이 뭔지 알아야한다.
Jwt
토큰은 유저의 신원이나 권한을 결정하는 정보를 담고 있는 데이터 조각이기 때문에 탈취를 당했을 때 Jwt 토큰을 탈취한 사람은 마치 신뢰할만한 사람인 것처럼 인증을 통과할 수 있고, 클라이언트와 탈취한 사람을 서버는 구분할 수 없기 때문에 최소한의 대비책으로 유효기간
을 필수로 두어야한다.
그런데 유효기간을 짧게 두면 사용자가 로그인을 자주 해야하기 때문에 사용자 경험적으로 좋지 않고, 유효기간을 길게 두면 보안상 탈취 위험에서 벗어날 수 없다.
그래서 나온 개념이 Access Token
과 Refresh Token
이다.
보통 Access Token
의 유효기간은 1시간에서 60일 정도로 정해진 기간은 없지만 비교적 짧게 설정이 되고,
Refresh Token
의 유효기간은 몇 십일에서 1년 정도를 길게 설정한다. 평소에 API와 통신할 때에는 Access Token을 사용하고, Refresh Token은 Access Token이 만료되었을 때 갱신을 하기 위해서 사용한다.
좀 더 구체적으로 서버와 클라이언트가 JWT토큰을 이용해서 통신하는 과정을 말해주겠다
Refresh Token
과 Access Token
을 서버로부터 받는다Refresh Token
과 Access Token
을 로컬에 저장해놓는다.헤더
에 Access Token
을 넣고 API 통신을 한다 (Authorization)Access Token
의 유효기간이 만료가된다.Access Token
은 이제 유효하지 않기 때문에 권한이 없는 사용자가 된다Access Token
을 받은 서버는 401 Unauthorized
에러 코드로 응답한다Access Token
대신 Refresh Token
을 넣어서 API를 재요청한다Refresh Token
으로 사용자의 권한을 확인한 서버는 응답쿼리 헤더에 새로운 Access Token
을 넣어서 응답한다Refresh Token
도 만료되었다면 서버는 동일하게 401 errorCode
를 보내고 클라이언트는 재로그인해야한다.그렇기 때문에 탈취자가 통신이 빈번히 일어나는 Access Token
을 탈취를 하더라도 유효기간이 짧기 때문에 보안을 높일 수 있는 대안이 되는것이다
내가 여기서 고민했던 점는 Access Token
과 Refresh Token
에 어떤 정보를 담아서 만들어야 하고, 어디에 저장을 어떻게 해야하는지였다.
우선 첫번째 후보는 로컬 스토리지 & 세션 스토리지 였다. 그러나 이렇게 저장한다면 자바스크립트로 토큰 값을 꺼내서 보내는 방식이기 때문에 XSS
공격에 취약하기 때문에 적절치 않다고 판단했다. 두번 째 후보는 쿠키이다. 쿠키 또한 자바스크립트로 접근이 가능하지만 HTTP Only 옵션과 secure 옵션을 설정한다면 보안을 높일 수 있지만 CSRF
공격에는 취약할 수 있다. 처음에는 그래도 다른 곳에 저장하는 방식에 비해서는 안전하다고 생각해 Access Token
과 Refresh Token
을 둘 다 쿠키에 저장을 하려고 했지만 Refresh Token
은 사용자의 Access Token
을 재발급 하는 용도이기 때문에 괜찮을 수 있지만 Access Token
또한 쿠키에 저장한다면 CSRF
공격을 통해서 인증 인가 과정으로 보호된 동작을 실행해버릴 수 있기 때문에 Refresh Token
만 쿠키에 저장하기로 결정했다
그리고 Access Token
은 Authorization Header
를 통해서 클라이언트에게 전달하기로 했다. 물론 완벽한 보안은 아닐 수 있지만 최소한의 위험만 감수하는 방법이라고 생각한다.