서버에서 클라이언트의 로그인 여부를 관리하는 방법으로 세션, JWT 등이 있다. 욜로가 서비스는 모든 로그인을 OAuth2.0 로그인으로만 접근이 가능하기 때문에 기본적으로 외부 서버에서 발급받은 토큰으로 로그인을 관리한다.
만약 세션
방식을 사용한다면 서버에서 생성한 세션 아이디가 key 값이 될 것이고, 외부 서버에서 발급받은 토큰들이 value 값이 될 것이다. MySQL Connection 의 점유를 줄이고 데이터 접근 속도를 높이기 위해 세션 저장소를 Redis 로 선택한다고 가정하자. 그러면 다음과 같은 형식으로 데이터를 저장할 것이다.
{
sessionId : {
accessToken : "~~~~",
refreshToken : "~~~~",
그 외 필요한 정보들..
}
}
전체적인 플로우를 생각해보자. 클라이언트의 수(접속한 수)만큼의 세션 아이디가 생성되고 저장된다. 이후에 세션 아이디를 포함한 요청이 들어왔을 때 일단 세션 아이디를 저장한 Redis 에 접근해야 한다. 그 이후에는 상황에 따라 알맞는 access token 이나 refresh token 을 조회하여 활용할 것이다. 즉, 어떤 요청이 들어오더라도 일단 세션 아이디가 적합한 값인지 확인하기 위해서는 Redis 에 접근해야 한다는 특징이 있다.
하지만 현재 상황에서 로그인 여부를 관리하기 위한 값으로 굳이 세션 아이디를 사용할 이유가 없다. 세션을 사용함으로써 얻을 수 있는 이점은 1. 세션 아이디로 값을 주고 받기 때문에 비교적 보안에 유리하다. 2. 브라우저 종료 시 세션 정보가 삭제되기 때문에 짧은 로그인 유지 시간을 지킬 수 있다. 세션 아이디 대신에 대체할 값으로 사용자의 고유 아이디가 있다. 이는 외부에 노출되어서는 안되는 민감한 정보가 아니므로 1번을 신경쓸 이유가 없다. 2번은 세션의 강력한 특징이지만 오히려 로그인을 자주 종료시키는 방식은 사용자에게 불편한 경험을 주게 될수도 있다. 따라서 굳이 세션 아이디가 아닌 사용자 식별값으로 대체하였다.
{
memberId : {
accessToken : "~~~~",
refreshToken : "~~~~",
그 외 필요한 정보들...
}
}
하지만 token 저장소로 Redis 를 선택하다보니 약간의 아쉬운 점이 생겼다. Redis 의 특성 상 value 에 접근하기 위해서는 key 와 hash key 의 값이 모두 필요하다. 즉, access token 에 접근하기 위해서는 memberId 의 값이 필요하다. 다만 여기에서 memberId 의 값이 필요하게 된 상황은 token 저장소로 Redis 를 선택했고, 데이터 자료구조를 위의 예시처럼 잡았기 때문이라고 생각하였다. 서버에서 결정한 정책 때문에 발생한 일이었고, 이처럼 클라이언트에게 요구되는 정보가 늘어날 상황이 또 발생하게 된다면 계속해서 API 스펙이 변경될거라 생각하여 이를 해결할 방법으로 JWT
를 이용하였다.
서버 내부에서 로그인 관리에 필요한 정보를 JWT 에 같이 저장하여 클라이언트에게 응답하면 클라이언트 측은 JWT 만 관리하면 된다. 해당 서버를 사용하는 클라이언트 측에서는 응답받은 토큰이 외부 서버에서 발급받은 토큰인지, 욜로가 서버에서 발급한 JWT 인지 인지하고 있지 않아도 되도록 access token, refresh token 을 각각 JWT 로 감쌌다. 이렇게 외부 서버에서 발급받은 토큰을 욜로가 서버에서 다시 한번 감싸면서 얻는 이점이 세 가지가 있었다.
서버 내부에서 필요한 정보를 API 스펙 변경 없이 쉽게 추가할 수 있다.
실제로 설계의 미숙함으로 클라이언트에게 받아와야 하는 정보가 추가되는 상황이 발생하였다. 사용자가 어떤 OAuth 로그인을 했는지 알기위한 provider 정보가 필요했는데, 매번 클라이언트에게 받아와야 하는 번거로움을 줄이기 위해 JWT 에 같이 저장하여 관리하였다. 최종적으로 JWT 에는 token, memberId, provider 의 정보가 포함되어 있다.
두벅스 사용자 서버에서 발급한 JWT 를 욜로가 서비스 서버에서 사용할 수 있다.
비밀키로 서명하는 JWT 의 특징 덕분에, 해당 키를 공유하여 욜로가 서비스 서버에서도 JWT 를 허용할 수 있게 되었다. 즉, 서버가 분리된 환경에서도 JWT 에 대한 검증을 진행할 수 있다.
외부 서버(또는 Redis)의 요청이 필요한 경우 욜로가 서버에서 한단계 검증 단계가 추가되어 외부 서버(또는 Redis)의 접근을 줄일 수 있다.
access token 을 갱신하려 한다고 가정해보자. 클라이언트는 욜로가 서버에서 생성한 refresh token 의 JWT 로 요청을 보낼 것이다. 1차적으로 이 JWT 가 실제로 욜로가 서버에서 발급한 토큰이 맞는지 secret key 로 검증을 할 것이다. 그 다음 Redis 의 저장 여부로 유효하며 적절한 토큰이 맞는지 검증한다. 모든 검증이 끝난 이후에야 외부 서버에게 요청하여 안전하게 access token 을 갱신한다. 최대한 자원을 덜 소비하기 위해 간단한 검증을 거친 이후에야 외부 서버의 Connection 을 열어주어 여러번 필터링을 거칠 수 있다.
토큰 검증 - Redis 저장 여부 검증 - 외부 서버 요청
로그인 이후 외부 서버와의 통신에서 사용하는 refresh token 과는 달리 access token 은 발급 받은 이후에는 만료 시간 외에는 (욜로가 서비스에서는..) 활용 과정이 없다. 또한 access token 은 비교적 만료 시간이 짧기 때문에 이를 DB 에 저장하여 관리하면 변경사항을 적용하기 위한 접근이 불필요하게 증가할 수 있다. 따라서 access token 은 토큰 자체 검증만을 수행하고 별도로 저장소에서 관리하지 않음으로써 Redis 의 접근량을 줄였다.
{
memberId : {
refreshToken : "~~~~",
그 외 필요한 정보들...
}
}
전체적인 토큰 사용 흐름은 다음과 같다.