💻 Backend
인증 서비스 탐험일지
Poom 서비스에 기반하여 작성되었습니다.
Option 1: 세션과 토큰
두 선택지 중에서 우리는 토큰 방식을 선택합니다.
이유는 App 서비스를 목표로 하기 때문에 세션보다는 더 적합한 방식을 선택하였습니다.
Option 2: 인증과 인가
우리는 사실상 ‘인증’만 합니다.
여기서 인증은 ‘회원이냐 아니냐’를 따집니다.
인가를 하지 않는 이유는 당장 필요가 없기 때문입니다.
인가는 ‘해당 기능에 접근할 권리가 있는 회원이냐 아니냐’이고 우리는 회원 등급제 또는 레벨을 두지 않고 있습니다.
Option 3: 간편 로그인과 자체 인증서버
우리는 2가지를 모두 구현했습니다.
다만 완성도에서 차이가 있습니다.
자체 인증서버를 구현했으나, 소위 ‘인증 서버’라고 할 때 필요한 모든 기능을 구현하기엔 리소스가 부족합니다.
때문에 간편 로그인과 병행하여 인증의 부족한 부분을 채웠다고 보시면 됩니다.
Option 4: 간편로그인 설계
간편 로그인에서 제공하는 기능 중에서도 어떤 걸 사용할 지 정해야 했습니다.
우리는 카카오 로그인을 사용하면서, 가장 기본 권한과 함께 플러스 권한으로 ‘유저 이메일’권한을 요구했습니다.
이는 ‘유저를 구분할 유니크한 값’이 필요했기 때문입니다.
(후에 알기로 카카오의 모든 유저가 이메일을 소유하진 않았다고 하여 새로운 정책이 필요합니다)
또, 프론트엔드와 백엔드가 어떻게 역할을 나누었는지 궁금하시죠?
- Frontend
카카오 인증서버로 인가코드 발급을 요청하는 역할을 수행합니다.
- Backend
카카오 인증서버로 액세스 토큰 발급을 요청하는 역할을 수행합니다.
카카오 인증서버로 유저 리소스를 요청하는 역할을 수행합니다.
때문에 전체 플로우는 다음과 같습니다.
카카오에서 제시한 플로우와는 차이점이 있습니다.
카카오에서는 인가코드 발급 응답을 클라이언트가 아닌 서버로 리다이렉트하길 권장합니다.
사실 그렇게 했어야 맞습니다… 😱
토큰을 가지고 회원가입과 로그인을 수행한다는 개념에 사로잡혀 오해를 했습니다.
토큰 방식에서는 로그인이 없습니다 😢
이 부분은 회고 포인트이자 아쉬운 부분이에요 - 후에는 이 사실을 알고 더 나은 개발자가 되어..
Option 5: 자체 인증 설계
이제 JWT 가 왔습니다~
기능으로 구분하면 다음과 같습니다.
- 액세스 토큰 발급
- 리프레시 토큰 발급
- 액세스 토큰 인증
- 액세스 토큰 재발급
여기서 회원의 기능을 더할 수 있어요.
- 필요한 정보를 추가로 받아서 회원가입
- 로그아웃
위에서도 말한 것처럼, 토큰 방식에는 로그인이 존재하지 않습니다.
회원가입과 로그인이라는 건 WEB 환경에서 유저로써의 경험에 익숙해진 나머지 떠올리는 클리셰입니다.
우리는 개발자니까 개발자의 관점으로 봐야합니다.
개발에서는 회원가입과 로그인 따위.. 없습니다.
그저 발급과 인증 뿐입니다 🧑🏽⚖️
이쯤에서 사소하지만 아쉬운 점 1차 정리
먼저 3번째 말하는데 토큰 방식의 개념을 제대로 이해하지 못한 채 접근하면서 카카오에서 권장하는 플로우를 스스로 꼬아버린 점… 생각할 수록 아쉽습니다.
두번째로는 API 설계입니다.
확장성을 고려했을 때 클라이언트로부터 받는 API는 그냥 [로그인] 이었으면 충분했다고 봅니다.
현재는 kakao 라는 단어를 api path 에 적용시켜 둔 상태에요.
이러면 다른 간편로그인이 추가될 때 API를 또 생성해야된다는 걸 의미합니다.
게다가 카카오 간편로그인을 지워버린다면 API 하나를 통으로 날려야하죠..
적당히 Header 값으로 구분했거나 등등 구분점을 설계시에 챙겨갔다면 login 이라는 path 하나만으로 서버가 알아서 naver, kakao, google 등의 로그인을 할 수 있었을 거라고 생각합니다. (확장성)
세번째는 JWT 그 자체를 이해했어야한다는 겁니다.
보통의 블로그들은 아무래도 배우는 사람들인 만큼 그저 복붙한 이야기에 불과합니다.
때문에 “왜?”에 대한 이야기 없이 “~이렇게 하면된다.” 만 있어요.
사실 개발자에게 근거 없는 주장은 실효성이 떨어집니다.
그러나 그런 주장을 급한 마음에 Accept 해버린 제 잘못입니다..
순수하게 JWT를 구성하는 라이브러리를 알아보고 스스로 구현 방안을 떠올린 뒤에 타 사례들과 비교했다면 더 합리적인 선택이 가능했을 거라고 생각합니다.
예를 들어 리프레시 토큰과 액세스 토큰 중 발급의 우선순위는 어디인가? 토큰을 저장할 때 어떤 방식으로 저장하는 게 유리한가? 등 셀수 없이 많은 내적 질문이 있었으니까요.
Option 6: 액세스 토큰의 서브젝트
토큰을 발급해줄 땐 Subject 로 무엇을 담을지 정해야합니다.
jwt 에는 크게 3가지 페이로드가 존재해요.
- 발신자, 주체, 수신자
각각의 영역에 필요한 데이터를 담아서 토큰을 생성해냅니다.
우리는 발신자와 수신자에 대한 내용은 없어요.
이건 특이할 게 없는게, 보통은 없는 게 일반적입니다.
중요한 건 주체이죠.
StackOverFlow 에 보면 유저의 정보를 세 곳 중 어디에 담아야 하나요? 라는 글이 있습니다.
거기에 제대로 설명되어 있는데, 발신자는 서버이고 수신자는 클라이언트입니다.
때문에 유저의 정보는 반.드.시 Subject 에 담아야합니다.
(각자의 이해에 따라 발신하는 쪽에서 보내는 거니까 발신자에 담나..?
수신자가 수신하는 사람이니까 여기에 담으면 되나..? 등과 같은 오해가 있다고 하네요)
그 다음 떠올릴 문제는 ‘무엇을 담나’ 입니다.
보안 정책상 유저의 개인정보를 토큰에 담아선 절대 안됩니다.
단순한 식별자만 넣어야하는데, 그 식별자는 가능하면 ‘무의미’해야합니다.
즉, 식별의 역할만 가져가는 게 좋습니다.
그래서 우리의 품 서비스에서는 테이블의 pk 값을 삽입했습니다.
그럼 위조가 가능한 것 아니야??!!
가능합니다.
원래 토큰 방식이라는 게 100% 안전한 방식이 아니고, 그런 방식은 없으니까요.
단, “얘네 토큰 subject 가 pk값이래” 이거 하나만으로 뚫릴 순 없습니다.
시크릿 키도 알아야하고, 암호화 알고리즘을 뚫어야합니다.
게다가 subject 에 pk 말고 데이터를 더 넣을 수도 있는데 (과연 내가 pk만 넣었을까?)
Option 7: 리프레시 토큰의 기준점
그럼 리프레시에선 뭘로 기준을 가져갈 수 있을까.
많이 찾아봤는데, 아무래도 자율입니다.
그래서 저는 리프레시 토큰의 역할 그 자체에 집중했습니다.
- 리프레시 토큰 → 액세스 토큰을 재발급한다.
이게 이 녀석의 모든 것입니다.
때문에 기준점은 “랜덤”입니다.
어떤 기준점을 가져가던, 상관없고 심지어 몰라도 되니까요.
그저 유니크하기만 하면 됩니다 😂
Option 8: 토큰에도 위아래가 있다
이것도 블로그마다 다른데, 처음에 저는 아예 독립적인 요소로 생각했습니다.
액세스는 액세스대로, 리프레시는 리프레시대로 발급해주는 방향이었는데, 이러면 문제가 있더라구요?
액세스가 만료되고 나면 리프레시를 가지고 재발급을 해줘야하는데, 리프레시의 기준점이 랜덤이니까 어떻게 매칭시켜서 재발급을 해주나..
이 때, 한참을 고민하다가 리프레시 기준점을 랜덤 말고 특정 값을 넣을까.. 까지 고민했습니다.
그렇게 했으면 큰일 날뻔 했죠. 💦
Redis 를 활용하면 됩니다.
토큰이라는 걸 사용하는 최고의 장점이 애초에 Stateless 인데, 서버에 저장을 한다?
이건 무조건 안된다고 생각했기 때문에 쳐다도 안 본 선택지였습니다.
그러나 방향을 잃자 일단 들어나 보자 하고 본 것이, 상당히 매력적인 솔루션이었습니다.
Redis 의 특성을 떠올려보면 서버에 저장한다고 해서 Stateless 를 완전히 놓아버렸다고 보긴 힘듭니다.
Redis 의 특징 2가지를 토큰에 잘 접목시킬 수 있으니까요. 궁합이 좋아요.
- 인메모리라서 빠르다.
- TTL 을 걸어둘 수 있다.
리프레시 토큰은 ‘잃어버려도 됩니다’
때문에 Redis 에 저장하기 ‘불안한’ 데이터가 아니에요.
게다가 오히려 시간이 지나면 제거해야 합니다.
이를 만약 Mysql 에 넣는다면 주기적으로 삭제 배치를 돌려야 하는 수고가 발생합니다.
게다가 리프레시 토큰은 액세스 토큰에 비해 생명 주기가 길기 때문에 요청이 자주 들어오는 것도 아니고, 역설적으로 서버에 저장을 해둬야 “강제 로그아웃” 기능을 구현할 수 있게됩니다.
때문에 Redis에 리프레시 토큰을 넣어주게되면 조회할 key값이 필요하고 이 key를 쓰려면?
액세스 토큰보다 먼저 발급되어야 하고 이후 액세스 토큰이 이 key와 연관된 무언가를 가져가야합니다. (자세한 설명은 생략)
그렇기에 결국 토큰에도 위아래가 있습니다!
Option 9: 품의 인증 설계 쁠로우
그럼 이제 플로우를 보아야겠죠?
카페24에서 불펌했습니다 ㅋ
좋은 레퍼런스더군요!
본인들의 제휴사에 제공하기 위한 자료라서 그런지 저 플로우차트 외에도 정책적인 것까지 정리를 잘해둬서 최대한 따라가려고 합니다 path 까지도..!
보고 싶다면 이 링크를 누르세요 😊
차이점은 토큰 만료시 4번과 같은 에러가 아닌 UnAuthorize 401 익셉션을 던집니다.
이유는 기본을 최대한 활용하고 커스텀을 멀리하려고 했기 때문인데, 필요하다면 얼마든지 추가 도입이 가능합니다.
Option 10: 초간단 API 명세
도메인이나 버전은 제외했습니다.
/auth/signup
: 회원가입 API#리퀘스트
{
"email": "korea@poom.com",
"nickname": "왓어뷰티풀라이프",
"photo": "abcdefg.jpg"
}
#리스폰스
header;
set-cookie: refresh-token: XXXX
{
"accessToken": "xxxxxx"
}
#예외
요청에 null 또는 블랭크는 차단됩니다.
/auth/login/kakao
: 로그인 API#리퀘스트
{
"code": "xxxxxx",
"redirect-uri": "https://xxxxx",
}
#리스폰스
header;
set-cookie: refresh-token: XXXX
{
"accessToken": "xxxxxx"
}
#예외
요청에 null 또는 블랭크는 차단됩니다.
/auth/token
: 액세스 토큰 재발급 API#리퀘스트
header
Authorization: bearer accesstoken
refresh-token: xxxxxx
body X
#리스폰스
header;
set-cookie: refresh-token: XXXX
{
"accessToken": "xxxxxx"
}
#예외
리프레시 토큰도 만료되었을 수 있습니다.
이 경우 로그아웃이 된 상태로 간주하고 다시 로그인 시도를 해야합니다.
/auth/logout
: 로그아웃 API#리퀘스트
header
Authorization: bearer accesstoken
refresh-token: xxxxxx
body X
#리스폰스
204 No_Content
#예외
강제 로그아웃 입니다 (리프레시 토큰 삭제)