계정과 관련해서 서버를 프로그래밍할 때에는 인증
과 인가
를 어떻게 할지가 중요한 고민 이슈다. 이 둘은 비슷해보이지만 다른 개념이다.
: 인증은 로그인 자체이다. 내가 이 사이트에 가입된 회원임을, 즉 특정 서비스에 일정한 권한이 주어진 사용자임을 아이디왜 패스워드 등을 통해서 말 그대도 인증을 받는 것이다.
: 반면 인가는 한 번 인증을 받은 사용자가 이후 서비스의 여러 기능들을 사용할 때, 즉 이를테면 내가 페이스북에 로그인으로 인증하고 "나서" 내 친구목록을 보거나 피드에 글을 작성하거나 좋아요나 댓글을 다는 등 내 계정으로만 할 수 있는 활동을 시도할 때 페이스북이 내가 로그인 되어있음을 알아보고 허가를 해주는 것이다. 다시말해 로그인이 유지되는 상태에서 일어나는 일이다.
오늘 알아볼 자세히 알아볼 JWT는 인가(Authorization)에 연관된 기술이다.
어떠한 웹사이트를 다수의 사람들이 이용할 때, 이 사람들중 어떤 사람들은 로그인을 한 상태고, 다른 사람들은 로그인 없이 방문 상태다. 서버는 각 요청이 들어올 때마다, 이를 보낸 사용자가 인증 과정을 거친 상태인지 확인해서 그에 따라 로그인이 필요한 기능들에 허용을 해줄지 말지를 결정해서 응답해야한다.
그렇게 하기에는 로그인이라는 것이 꽤 무거운 작업이다.
데이터베이스에 저장된 사용자 계정의 해시값 등을 꺼내온 다음에 이것들이 사용자의 암호를 복잡한 알고리즘으로 계산한 값과 일치하는지 확인하는 과정을 거쳐야한다.
계산 자체도 무겁고, 데이터베이스에서 무엇을 꺼내오는 것이 시간과 자원을 많이 잡아먹기 때문에 그것을 매 요청마다 하는 것은 무리다.
결정적으로 매 요청마다 ID과 PW가 실려서 날아다니면 보안상 위험하기도 하다.
그래서 세션과 토큰이라는 방식이 있는것이다.
기존의, 전통적으로 많이 사용되어온 방식이다.
사용자가 로그인에 성공하면, 서버는 '세션 표딱지'라는 것을 출력한다. (영화관 티켓의 개념과 유사) 이것을 쭉 찢어서 한 쪽은 사용자 브라우저로 보내고, 한 쪽은 서버의 메모리에 올려놓는다. 경우에 따라서는 하드디스크에 넣거나, 데이터베이스에 넣어놓기도 한다.
(넣고 꺼내는 작업이 얼마나 가볍고 빠르냐에 따라서 메모리는 책상, 하드는 서랍, 데이터베이스는 창고에 비유할 수 있다. 그리고 메모리가 나머지 둘보다 훨씬 빠르다.)
그런 표 반쪽을 받은 쪽은 크롬이나 엣지같은 브라우저인 것다. 브라우저는 이 표를 SessionID라는 이름의 쿠키로 저장하고, 이 브라우저는 앞으로 사이트에 요청을 보낼때마다 이 표딱지를 실어서 보낸다. 그러면 이제 요청에 이 세션 아이디란게 실려오면 서버는 그것을 메모리에서 맞는 짝이 있는지 찾아서 있으면 인증을 해주는 것이다.
이처럼 이 SessionID를 사용해서 어떤 사용자가 서버에 로그인 되어있음이 지속되는 이 상태를 "세션" 이라고한다.
메모리에 표딱지를 넣어놓기 때문에 사용자가 동시에 많이 접속을 하면 메모리가 부족해질 것이고, 그리고 이 메모리라는 것은 서버에 문제가 있어서 꺼져버리거나 하면 휘발성이기 때문에 모두 날아간다. 즉 서버가 재부팅 되어야하는 상황이오면 서버에 있는 것들은 다 날아가게 된다. 그러면 모든 사용자들이 다시 로그인을 해야한다. 그렇다고 이것을 하드에 저장하자니 메모리에 저장하는 것에 비해 더 느릴 것이다.
더 골치아픈 경우는 서비스가 어느정도 규모가 있어서 서버를 여러대를 두고 사이트를 운영할 때이다. 서버가 여러 개라는 것은 서버가 각자 메모리와 하드를 두고 일을 하고 있고, 손님들이 들어오면 그 요청들을 여러 서버들 사이에서 분산해서 로드밸런싱해줘야하는 것인데, 만약 로그인이 1번서버에서 이루어지고, 이메일 페이지로 가는 요청을 3번 서버한테 간다고하면, 3번 서버는 그 사용자의 표딱지가 없으니까 세션 유지가 제대로 안되는 것이다.
다른 방법으로는 이 표딱지 반쪽을 공용창고인 데이터베이스에 넣어두거나 (속도가 많이 느려짐) 더 흔히는, 레디스나 MamCached같은 메모리형 데이터베이스 서버 즉 길다란 공용 책상을 따로 둬서 그 위에다 올려두기도 한다. 하지만 이도 결국에 껐다켜지면 정보가 날아가 다시 로그인해야한다.
이처럼 서버가 복잡한 구성과 환경에서 어떤 상태를 기억해야된다는 것이 굉장히 설계하기에 머리아픈 것이다. 그래서 그런 부담 없이 이 인가를 구현하기 위해 고안된 것이 "토큰 방식"
인 JWT
인 것이다.
JWT를 사용하는 서비스 에서는 이제 사용자가 로그인을 하면 토큰이라는 표
를 출력해서 건네준다. 이번에는 찢어서 주지 않고 그냥 준다. 즉 서버가 뭔가를 기억하고 있지 않다
는 이야기이다.
토큰은 다음과 같이 알바벳과 숫자들이 섞인 엄청 긴 문자열이다.
https://jwt.io/
인코딩 또는 암호화된 3가지 데이터를 어어붙인 것이다.
잘보면 중간에 마침표가 두 군데 들어있다. 이 마침표를 기준으로 3부분으로 나뉜다.
각각 header
, payload
, verify signature(서명)
로 구분된다.
payload부분을 Base64로 디코딩해보면 JSON형식으로 여러 정보들이 들어있다.
이 토큰들을 누가 누구에게 발급했는지, 이 토큰이 언제까지 유효한지, 서비스가 사용에게 이 토큰을 통해 공개하기 원하는 내용(사용자 닉네임, 서비스상의 레벨, 관리자 여부 등)을 서비스 측에서 원하는 대로 담을 수 있다. 이렇게 토큰에 담긴 사용자 정보 등의 데이터
를 Claim
이라고 한다.
사용자가 로그인을 하고 나서 받는 토큰에 이 정보들이 클레임이라는 걸로 실려온다는 것이다.
이게 그 이후 요청들마다 이번에는 사용자로부터 서버한테 보내지는 것이다.
사용자가 가지고 있는 토큰 자체에 이런 정보가 실려있므로 서버가 요청마다 일일이 데이터베이스에서 뒤져봐야할 것들을 줄일 수 있게 해준다.
하지만 특별한 암호화도 아니고 Base64로 인코딩되어 있는 거면 사용자가 다시 디코딩해서 보고 조작해서 악용할 수도 있을 것이다. 그것을 방지하기 위해 1번과 3번 파트가 있는 것이다.
이것을 디코딩해보면 두 가지 정보가 들어있다.
1. type
: 토큰의 타입인데 여기에는 언제나 JWT가 들어간다. 고정값이다.
2. alg
(중요) : 알고리즘의 약자인데 3번 서명값을 만드는데 사용될 알로리즘이 지정
된다.
HS256 등 여러 암호화 방식 중 하나를 지정할 수 있다.
1번 헤더와 2번페이로드, 그리고 '서버에 감춰놓은 비밀값' 이 셋을 이 암호화 알고리즘에 넣고 돌리면 3번 서명값이 나오는 것이다.
이 암호화 알고리즘이라는게 한쪽 방향으로는 계산이 돼도 반대쪽으로는 안되는 것이어서, 서버만 알고있는 그 비밀값을 찾아낼 방법이 없는 것이다.
그리고 글자 하나만 바뀌어도 3번값이 완전히 달라지기 때문에 2번 페이로드를 수정해서 유효한 3번 서명값이 나오려면 서버에 숨긴 비밀키를 알고 있어야 되니까 이것을 조작할 수가 없다.
서버는 요청에 토큰 값이 실려들어오면 1,2번의 값을 '서버의 비밀키'와 함꼐 돌려봐서 계산된 결과값이 3번 서명값과 일치하는 결과가 나오는지 확인한다. 3번 서명값과 계산값이 일치하고, 유효기간도 지나지 않았다면 그 사용자는 로그인 된 회원으로서 인가를 받는 것이다.
이런 방식을 사용하므로써 서버는 사용자들의 상태를 어디에다 따로 기억을 해둘 필요가 없이. 이 비밀값만 손에 쉬고 있으면 요청이 들어올때마다 토큰을 마치바코드 찍듯 스캔해서 사용자들을 걸러낼 수 있다.
이처럼 시간에 따라 바뀌는 어떤 상태값을 안 갖는 걸 stateless하다고 한다. 세션은 반대로 stateful하다.
세션을 완전히 대체하기에는 JWT에도 큰 결점이 있다.
세션처럼 stateful해서 모든 사용자들의 상태를 기억하고 있다는 건 구현하기 부담되고 고려사항도 많지만, 이게 되기만 하면 기억하는 대상의 상태들을 언제는 제어할 수 있다는 의미이다.
예를 들어서 한 기기에서만 로그인이 가능한 서비스를 만들려고 하는 경우 pc에서 로그인한 상태의 어떤 사용자가 핸드폰에서 또 로그인을 하면 pc에서는 로그아웃 되도록 기존 세션을 종료할 수 있는 것이다.
세션 방식에서는 메모리에 올려둔 기존 표딱지를 버리기만하면 되는데, JWT에서는 그런게 불가능하다. 이미 줘버린 토큰을 뺏을 수도 없고 크 토큰의 발급 내역이나 정보를 서버가 어디 기록해서 추적하고 있는 것도 아니기 때문이다.
즉 내가 쥐고 있을 필요가 없어서 편하긴하지만 통제는 할 수 없다는게 JWT의 특징이다.
또 더 심한 경우 내 토큰이 해커에게 도난당해도 가져가버린 토큰을 무효화할 방법도 없다는 것이다. 그래서 실 서비스중에 JWT만으로 인가를 구현하는 곳은 생각보다 많지 않다.
이런 것을 보완하기 위한 방법으로 만료시간을 가깝게 잡아서 토큰의 수명을 아주 짧게 주는 방법이 있다. 그런데 이렇게 하면 계속 로그인을 해야한다.
그래서 이를 편리하게 이용하기 위에 로그인을 하고나면 토큰을 두개 준다.
하나는 수명이 몇 시간 몇분 이라로 짧은 access토큰
과 나머지 하나는 꽤 길게, 보통 2주 정도로 잡혀있는 refresh토큰
이다.
access토큰
과 refresh토큰
을 발급하고 클라이언트에게 보내고 나서 refresh토큰
은 상응 값을 데이터베이스에도 저장한다. 클라이언트는 access토큰
의 수명이 다하면 refresh 토큰
을 보낸다.
서버는 그것을 데이터베이스에 저장된 값과 대조해보고 맞다면 새 access토큰
을 발급해주는 것이다.
이제는 이 refesh토큰
만 안전하게 관리된다면 이게 유효할 동안은 access토큰
이 만료될 때마다 다시 로그인할 필요 없이 새로 발급받을 수 있는 것이다.
결론적으로 매번 인가를 받을 때 쓰는 수명 짧은 토큰
이 access토큰
이고, 엑세스 토큰을 재발급받을 때 쓰는 것
이 refresh토큰
이다.
이렇게하면 중간에 access토큰
이 탈취당해도 오래 쓰지는 못하게된다. 누구를 당제 로그아웃 시키려면 리프레시 토큰 지워서 토큰갱신이 안되게 막으면 되는 것이다. 하지만 그렇게 해도 짧게나마 access토큰
이 살아있는 동안은 이걸 바로 차단할 방법은 없다. 그저 토큰 하나로 쓰는 것보다는 훨씬 낫지만 이것도 완벽한 해결책은 아닌 것이다. 이런점이 현재 JWT의 한계이다.
때문에 JWT가 아무리 구현하기 편리하고 좋더라고 이를 적용하기에 내 서비스가 적한합지 충분히 고려를 해야한다.
참고문헌 : 얄팍한 코딩사전