-
JWT를 이용한 인증·인가 프로세스에대해 낱낱히 파헤쳐보자! feat. 토큰, 세션Flutter/study 2024. 2. 2. 13:42
로그인을 구현하면서 인증과 인가에 대한 부분은 개인적으로도 학습하고, 개발 팀원들과 많이 토론했던 부분입니다.
그때 얻은 인사이트와 학습한 내용을 토대로 정리해보겠습니다.✓ 인증(Authentication), 인가(Authorization)
우리는 배달의 민족을 사용하거나 네이버에 로그인할 때 등 어떠한 서비스를 이용하기 위해선 로그인 절차를 꼭 거쳐야 합니다. 로그인은 결국 인증(Authentication)을 하기 위한 행위입니다. 그렇다면 인증은 무엇을 의미하는 걸까요?
📌 인증 (Authentication)
- 유저가 서비스에 자신의 신원을 확인하고 증명하는 과정
- 서비스의 중요한 데이터나 리소스를 보호하고 불법적인 액세스로부터 방어할 수 있다.
- 인증된 유저는 서비스를 이용할 때, 리소스 및 기능에 대한 액세스 권한을 받을 자격이 생긴다.
서버는 사용자로부터 받은 이전 데이터와 사용자가 로그인 시에 입력한 데이터를 비교하여 인증 절차를 수행합니다. 이 과정에서 유저의 신원이 확인되면, 서버는 해당 유저에게 정해진 권한을 제공하며, 이를 통해 특정 리소스나 기능에 접근이 가능해집니다.
이처럼 인증은 서비스의 보안성을 유지하고 사용자의 개인 정보를 보호하는 데 중요한 역할을 합니다.
예를 들어 '에버랜드'에 놀러 갔다고 생각해 봅시다.
우리는 에버랜드에 입장할 때 이용권을 반드시 구매해야 한다. 그리고 놀이공원을 돌아다니다가 회전목마를 타려고 하면 손목에 있는 이용권을 직원에게 보여주고, 직원은 회전목마를 탈 수 있는 이용권인지 확인을 하고 나서 우리를 빈자리로 안내한다.
로그인을 하는 것은 매표소에서 표를 사는 행위와 유사합니다.
- 에버랜드에 입장할 때 매표소에 직접 방문(신원확인)하여 이용권을 구매하고 입장 ➡️ 서버가 데이터베이스에서 유저정보를 확인 후, 유저에게 앱 이용권을 발급해 주고 서비스 진입
그리고 놀이기구를 탈 때마다 직원이 이용권을 확인하는 작업이 자연스럽게 진행되죠.
- 직원이 이용권을 확인 후, 빈자리로 안내 ➡️ 서버가 앱 이용권을 확인 후, 기능을 수행
위의 과정은 서비스가 유저에게 인가를 해주는 것과 비슷합니다. 그렇다면 인가는 무엇일까요?
📌 인가 (Authorization)
인증된 유저는 서비스를 이용할 때, 리소스 및 기능에 대한 액세스 권한을 받게 됩니다.
그 뒤에 기능을 수행할 때 액세스 권한을 확인하는 절차를 바로 인가라고 합니다.
정리하자면 "서비스를 이용하기 위해 본인을 증명하는 것"을 인증이라고 하며, "어떤 작업을 수행할 때 해당 리소스 및 기능에 대해 액세스 권한이 있는지 확인하는 행위"는 인가입니다.
🤷🏻♂️ "에버랜드에 들어가는 순간 놀이공원 밖에 나가지 않고 안에서 돌아다녀야 하는 것처럼, 웹사이트나 앱에서는 인증이 되었는데도 굳이 인가를 거치는 행위가 필요한가요?"
웹사이트나 앱은 HTTP를 통해 서버와 유저가 통신을 하게 됩니다. 그리고 HTTP에는 Stateless한 특징이 있습니다.
Stateless한 특징으로 인해 각각의 클라이언트 요청이 서버에 의해 독립적으로 처리되며, 서버는 클라이언트의 이전 상태를 알지 못합니다. 즉, 인증이 되었다고 해도 인증상태를 유지하고 있는지는 서버에서 알 수가 없고, 각각의 요청은 이전 요청과 독립적입니다.
따라서 이를 위한 특별한 프로세스가 없다면 '인가'뿐만 아니라 '인증'조차도 api를 사용할 때마다 수행되어야 합니다.
이런 귀찮은 문제점을 해결하는 방법으론 대표적으로 세션과 토큰이 있습니다.
✓ Session vs Token
서버가 클라이언트의 인증상태를 알 수 없기 때문에 인증된 유저의 정보는 어딘가에 그 정보를 저장하고 있어야 합니다.
세션과 토큰은 여러 차이점이 있지만 가장 큰 차이점은 인가에 필요한 정보를 저장하고 관리하는 방식이 다르다는 점입니다.
그렇기 때문에 인증을 하는 행위는 비슷하나 인가를 할 때 그 흐름이 다르게 나타납니다.
📌 Session
Session의 인증 세션의 경우 인증 정보를 관리하는 주체가 서버 메모리 또는 데이터베이스가 됩니다. 인증이 완료되면 서버에서는 session ID를 만들어 데이터베이스에 저장하거나 서버 메모리에서 관리하고 세션 ID는 쿠키의 형태로 클라이언트에 보내고, 클라이언트에서는 이를 저장하고 인가를 할 때마다 사용하게 됩니다.
Session의 인가 유저는 인가가 필요할 때마다 HTTP Cookie 헤더에 세션 ID를 같이 전송합니다. 전송받은 세션 ID 자체로는 아무런 정보가 없기 때문에 세션 ID를 통해 유저를 특정하고 권한을 가지고 있는지 확인을 한 후, 서비스 또는 리소스에 대한 액세스를 허용합니다.
✍🏼 Session의 단점
세션을 사용할 때, 세션 저장소를 어디에 둬야 할지 고민을 하게 되는데 이때 세션의 단점이 나타납니다.
- 세션 저장소를 데이터베이스(DB)로 설정하는 경우
- 단순히 검증, 검색만을 위해 DB에 접근하는 로직이 추가되기 때문에 성능 측면에서 비효율적인 방법인 것을 직관적으로 알 수 있음
- 세션 저장소를 서버 메모리로 설정하는 경우
- 세션이 많아질수록 서버에 부하가 커진다.
- 서버의 리소스가 부족할 경우 흔히 서버를 수평적으로 확장하는 로드밸런싱을 하는데 이때 세션 관리를 하지 않으면 세션 불일치 문제가 발생한다.
- 서버에서 장애가 발생되면 세션이 유실될 수도 있다.
메모리에 저장하여든 DB에 저장하든 리소스에 대한 부담이 발생하며, 클라이언트 상태를 저장하는 행위로 인해 Stateful한 상태가 되어 HTTP 통신의 비상태성(Stateless)에 위배될 수 있습니다.
하지만 서버에서 로그인 상태를 확인하기 유용하고 작업을 추적하고 관리하기 좋기 때문에 로그인 횟수가 적은 시스템에서 많이 사용되며, 중복 로그인이 불가능해야 하거나 단일 디바이스에서 로그인을 해야 되는 경우, 세션을 사용하기도 합니다.
오늘날에는 확장으로 인한 문제점을 세션 클러스터링, 스티키 세션 등을 사용하거나 인 메모리(In-Memory) DB 등을 이용해 세션과 토큰을 서비스 및 기능의 특성에 맞게 적절하게 섞어서 사용하는 경우가 많습니다.
📌 Token
토큰은 세션의 문제점을 해결하고 분산된 환경에서 확장성을 향상하기 위해 등장했습니다.
아래 그림과 함께 알아보도록 하겠습니다.
인증 토큰은 세션과 달리 인가에 필요한 정보를 클라이언트에서 관리하기 때문에 탈취의 위험이 있어 민감한 정보는 담지 않습니다.
토큰을 생성하는 방식은 세션과 크게 다르지 않습니다.
인가 유저는 인가가 필요할 때마다 HTTP Authorizetion 헤더에 Bearer 형태로 토큰을 같이 전송합니다.
서버는 토큰의 서명을 검증하여 유효성을 확인하고 필요한 경우 토큰 내의 클레임(claims)에서 인가에 필요한 정보를 추출 후, 추출된 정보가 올바르다면 서버는 해당 요청에 대한 기능을 수행합니다.
토큰을 이용하면 저장소를 따로 둘 필요가 없어 HTTP의 stateless 특징에 위배되지않아 서버를 확장할때 세션을 사용하면 일어나는 문제점들이 해소되며 인가를 위해 DB에 접근할 필요가 없어 세션에 비해 서버의 부하가 감소된다는 장점이 있습니다.
이러한 이유로 현대 웹·앱 개발에서는 토큰 기반의 인증 방식이 세션 기반의 인증 방식에 비해 더 많이 사용되고 있습니다.
그럼 저도 사용중이고 가장 많이 사용하는 토큰 종류 중 하나인 JWT에 대해 알아보겠습니다.
✓JWT(Json Web Token)
출처: jwt.io Token을 생성하는 것은 도메인 서버이긴하나 보안의 책임은 토큰을 관리하는 클라이언트에 있습니다.
토큰이 클라이언트에 존재함으로 인해 노출 및 탈취의 위험이 있기때문에 토큰 자체의 메커니즘이 중요합니다.
먼저 토큰 구현에 필요한 구성에 대해 알아보겠습니다.
- 암호화 및 서명
- 토큰은 적절한 방법으로 암호화되어야하며 서명을 통해 토큰의 무결성을 검증할수있어야함
- 엑세스 제어 및 권한
- 토큰에는 사용자의 권한에 대한 정보가 존재하므로 탈취되어도 해당 권한을 악용하기 어렵도록 적절한 제어 메커니즘이 필요함
- 토큰 만료 및 갱신
- 토큰은 일정 기간동안만 유효하며, 만료된 토큰은 새로운 토큰으로 갱신함으로서 토큰의 수명을 관리할수있어야함
- 클라이언트 저장 옵션
- 토큰은 클라이언트에서 안전하게 저장되어야함
- 인증 서버의 신뢰성
- 인증 서버는 안전한 키 관리 및 적절한 암호화 알고리즘을 사용하여 토큰을 안전하게 발급할수 있어야함
- 토큰 범위 제한
- 민감한 정보는 최소화하여 토큰의 크기는 줄이고 보안은 강화되어야함
JWT를 이용한 인증·인가 구현방식은 위의 조건을 모두 충족하면서 표준적이고 효율적인 방법 중 하나이며, 대부분의 서버 프레임워크는 JWT 양식의 토큰을 사용할수있는 라이브러리 또는 패키지가 존재합니다.
📌 JWT란?
공식문서에는 다음과 같이 정의하고있습니다.
"JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object."
"JSON Web Token(JWT)은 정보를 JSON 객체로 안전하게 전송하는 간결하고 자체 포함 형식을 정의한 개방형 표준입니다.(RFC 7519)"JWT는 Json 객체로 구성되어있고, 개방형 표준으로 정의되어있어 다양한 시스템과 플랫폼에서 사용 할 수 있습니다. 또한 자체적으로 필요한 정보를 내부에 포함하고있어 인가에 필요한 추가적인 DB조회 없이도 필요한 정보를 확인할수 있습니다.
JWT에서는 인가에 필요한 정보는 어떻게 포함하고있을까요?
📌 JWT의 구성
JWT는 헤더, 페이로드, 시그니처 세 부분으로 이루어져 있고 점(.)으로 이들의 경계를 표시하고있습니다.
✍🏼 HEADER (첫번째 부분)
헤더는 JWT의 타입과 사용된 서명 알고리즘에 대한 메타데이터를 담고 있습니다. 위와 같이 작성되며 Base64Url로 인코딩 되어있습니다.
✍🏼 Payload (두번째 부분)
페이로드는 토큰에 포함되는 클레임(Claims)으로 토큰의 실질적인 정보를 담고 있으며 3가지로 분류됩니다.
- Registreed claims
- 표준적으로 정의된 클레임으로 필수는 아니지만 권장되는 미리 정의된 클레임의 집합
- JWT는 콤팩트하게 유지되어야하므로 클레임 이름은 세 글자로 구성된다.
- iss(발행자), exp(만료 시간), sub(주제), aud(청중)
- Public claims
- JWT를 사용하는 개발자가 자유롭게 정의할수있는 클레임이다.
- 충돌을 피하기위해 IANA JSON Web Token 레지스트리에 정의되거나 충돌 방지가 가능한 네임스페이스를 포함하는 URI로 정의되어야 함
- Private claims
- 등록 및 공개되지않은 클레임으로, 사용하기로 합의한 당사자 간에 정보를 공유하기위해 만들어진 사용자 정의 클레임
- 주로 유저 역할(관리자, 일반 사용자 등)과 같은 정보를 포함함
실제로 자주 사용되는 부분은 Pubilc claims입니다.
보통 userId 등의 형태로 인가에 필요한 정보를 담고있습니다.
✍🏼 Signature (세번째 부분)
시그니처는 토큰의 무결성을 보장하고 메시지가 변경되지 않았음을 확인하기 위한 핵심 구성요소입니다.
시그니처 부분을 생성하려면 인코딩된 헤더, 페이로드, 비밀 키, 헤더에 지정된 알고리즘을 사용하여 서명해야합니다.
개인 키로 서명된 토큰의 경우 JWT의 발신자가 주장하는 대로 본인인지도 확인할 수 있습니다.
✍🏼 JWT 용량
어떤 서버에서는 토큰 용량을 8KB 이상 허용하지않는 경우도 있기때문에 토큰에 너무 많은 정보를 담지않도록 노력해야합니다.
✍🏼 JWT 전송 규칙
JWT를 사용하면 클라이언트가 리소스에 엑세스 할때마다 일반적으로 Bearer라는 스키마를 사용해야하며 Authorization 헤더에 다음과 같이 정해진 규칙에의해 전송되어야합니다.
Authorization: Bearer <token>
🤷🏻♂️ "꼭 HTTP 헤더에 JWT를 포함해서 줘야하나요? 쿠키의 형태로 전달하면 안될까요?"
쿠키는 쿠키 헤더의 Same-Site 속성으로 인해 일반적으로 동일 출처 정책에 따라 전송되어 보안상 다른 도메인 간의 요청에서는 자동으로 전송되지않습니다. 그래서 다른 도메인에 위치한 API나 서버와 통신해야하는 경우 브라우저의 CORS 정책에 의해 HTTP 통신에 문제가 생길 수 있습니다.
CORS: 브라우저에서는 보안상의 이유로 도메인, 프로토콜 포트번호가 하나라도 다를 경우 교차출처(cross-origin)라고 판단하고 요청을 제한한다. 만약 요청을 하려면 서버의 동의를 받아야하며 서버가 동의하면 브라우저는 요청을 허락하고 동의하지않으면 브라우저에서 거절하게된다. 이런 메커니즘을 CORS(Cross-Origin Resource Sharing)이라고 부른다.
하지만 쿠키를 사용하지않고 JWT를 HTTP의 Authorization 헤더에 담아서 전달하게되면 CORS로 인한 이슈는 애초에 발생하지않게 됩니다.
📌 OAuth 2.0 프로토콜
로고 출처: 미리 토큰 정책을 사용하는 대표적인 케이스로 소셜로그인에 사용되는 OAuth 2.0 프로토콜이 있습니다.
네이버 클라우드 플랫폼 공식문서에서는 OAtuh 2.0에 대해 이렇게 소개합니다.
Open Authorization 2.0은 웹 및 애플리케이션 인증 및 권한 부여를 위한 개방형 표준 프로토콜 입니다. 이 프로토콜에서는 서드파티 애플리케이션이 사용자의 리소스에 접근하기 위한 절차를 정의하고 서비스 제공자의 API를 사용할 수 있는 권한을 부여합니다.
OAuth 2.0의 장점은 사용자가 각 서비스에서 개인정보를 반복해서 입력하지 않아도 되도록 하며, 동시에 보안적인 측면을 고려하여 서로 다른 애플리케이션 간에 인증된 사용자 정보를 안전하게 공유하는 게 가능하다는 점입니다.
그런데 토큰은 클라이언트에서 관리하기때문에 탈취 당할 위험이 늘 존재합니다. 따라서 토큰을 자주 재발급받아 탈취 당해도 보안상 큰 이슈가 생기지않도록 미리 방지하는 것이 필요하죠.
하지만 토큰을 재발급 받기위해 인증을 하는 행위는 대략 "로그인을 3분이나 5분 간격으로 하는 것"과 마찬가지이기때문에 유저에게 좋지않은 사용자 경험을 줄수있습니다.
이런 보안상의 이유로 인해 OAuth2.0에서는 Refresh Token이라는 개념을 처음 도입하게 되었습니다.
✍🏼 Refresh Token
Oauth2.0에서는 엑세스에 사용되는 토큰의 유효기간을 짧게 함으로서 보안성을 높여주고 시간이 지나 유효하지않는 토큰이 되는 순간 또다른 토큰으로 엑세스에 필요한 토큰을 재발급해주는 프로세스가 존재합니다.
이때 엑세스에 사용되는 토큰을 accessToken이라고 하고, 엑세스토큰을 재발급하기위해 사용되는 토큰을 refreshToken이라고 합니다.
- Access Token
- 리소스에 접근하기위해 사용되는 토큰을 의미하고, 사용자에게 권한을 부여하는 역할을 한다.
- 일반적으로 짧은 유효 기간을 가지고 있으며 만료되면 다시 갱신해야한다.
- Refresh Token
- 리프레시 토큰은 엑세스 토큰의 갱신에 사용되는 토큰이며 단순히 갱신에만 사용되기때문에 굳이 특별한 정보를 담을 필요가 없는 것이 특징이다.
- 보통 인증(로그인)시 엑세스 토큰과 함께 클라이언트로 전달되며 Oauth2.0 명세에는 없지만 대략 한달이내의 유효기간을 가진다.
- 리프레시 토큰도 만료가 된다면 시스템에서는 다시 인증(로그인)을 요구한다.
리프레시토큰을 이용함으로 인해 보안성을 유지하고 클라이언트와 인증 서버 간의 신뢰성을 강화할수있습니다.
그렇기때문에 JWT를 채택한 서비스에서는 기본적으로 accessToken과 refreshToken을 모두 사용하고 있습니다.
그럼 실제로 엑세스 토큰 갱신은 어떻게 진행될까요?
만료된 엑세스토큰으로 장바구니 데이터를 요청하는 시나리오
🤷🏻♂️ "만료가 된 시점에 굳이 에러를 반환해서 재발급 받도록 해야할까요? 서버에서는 이미 만료된 것을 알고있으니 반환할때 엑세스 토큰과 장바구니 데이터를 함께 반환 해주면 안될까요?"
통신이 너무 많이 일어나는 것 같아 성능을 위해 단계를 줄이는 방법으로 실제로 프로젝트를 진행할때 서버측 팀원이 제시했던 의견이었습니다. 하지만 위의 방안은 다음과 같은 문제점이 있습니다.
- 보안상의 문제
- 리프레시 토큰이 없이 만료된 엑세스 토큰으로 얼마든지 갱신된 토큰을 리턴받을수 있다는 것은 만료된 엑세스토큰이 계속 재사용이 가능하다는 것을 의미합니다. 이는 보안에 취약성을 띄게되고 악의적인 공격에 노출 될 수 있습니다.
- Stateless 원칙 위배
- 재사용이 가능하다는 것은 만료된 토큰 그 자체로서 stateful한 특징을 가지게 되므로 HTTP의 Stateless 원칙에 위배됩니다.
- 리프레시 토큰 미사용
- 리프레시 토큰을 이용한 엑세스토큰 재발급 로직이 필요없으므로 리프레시 토큰을 사용한 보안 기능을 사용할 수 없게 됩니다.
🤷🏻♂️ "엑세스 토큰이 만료 되는 시간을 클라이언트에서 미리 알고있다면 통신요청을 하기전에 만료되기 직전의 엑세스토큰을 갱신하는 요청부터 하는 방법이 좋지 않을까요?"
이번에도 통신 단계를 줄이고자 프론트 팀원이 제시했던 의견입니다. 여기에도 저는 다음과 같은 문제점을 제시했습니다.
- 유저의 요청 행동을 예측할 수 없다.
- 토큰이 만료 될 것을 미리 알고싶다면 유저가 만료되기전에 인가 요청을하는 행위가 보장되어야한다. 예를들어 엑세스토큰 만료시간이 3분이라면 유저는 반드시 3분안에 기능을 수행해야하는데 사실상 특정 할 수 없기때문에 만료에 대한 에러처리는 피할 수 없다.
- 만료 시간이 변경 될 때마다 대응하기 어렵다.
- 만료시간을 설정하는 것은 서버에서 동적으로 이루어진다. 만약 사내의 정책으로 인해 만료시간을 수정하게 된다면 클라이언트 측에서 대응하기위해 코드를 수정해야하는 번거로움이 발생한다. 어떤 이유든 서버와 결합도가 높아지면 유지보수와 테스트 측면에서 불리해진다.
드디어 JWT에 대한 글이 마무리가 되었습니다.
인증과 인가에 대한 내용은 각자 공부한 것을 바탕으로 프로젝트를 진행할때 팀원들과 5시간 넘게 재밌게 회의했던 기억이있습니다.
마지막에 팀원들과 주고받은 의견에대해 제 생각을 좀 더 적자면 JWT와 같이 문서에도 기재된 고착화된 플로우에 대해 개선사항을 생각할때는 굉장히 보수적으로 이루어져야한다고 생각합니다.
물론 빠른 속도로 IT가 발전하면서 개발에대한 메커니즘이 변경될 순 있지만 현 시점에도 정석처럼 사용되는 방법에는 개인적으로 그만한 이유가 있다고 생각하고 최대한 새롭게 제시된 의견에서 문제점을 찾으려고 노력하는 편입니다.
그렇기때문에 화자가 정해진 플로우를 벗어나는 의견을 제시할때는 학습 후, 깊게 고민하고 명확하게 전달해야하며, 청자는 마치 레드팀이 된것처럼 정확한 반론과 근거를 제시하는 것이 중요하다고 생각합니다.
이렇게 많은 의견을 제시해보고 검증된 획기적인 아이디어들은 팀에서 열린 마음으로 채택하는 자세도 협업 및 팀프로젝트에서 굉장히 중요하다고 생각합니다.
많은 개발적 브레인스토밍이 일어났던 주제여서 글이 길어졌습니다.
다음은 실제로 JWT를 이용해 flutter에서 적용한 방법과 자동로그인 구현에 대해 포스팅 해보겠습니다.
긴글 읽어주셔서 감사합니다.
[Project] 로그인&회원가입 기능을 설계할 때, UX관점에서 고려해야하는 부분
소셜 로그인은 왜 필요한가? 로그인과 회원가입의 경우 앱의 성격에 따라 굉장히 다양하게 흘러간다. '미리'를 제작할 때 비슷한 서비스를 제공하는 앱들을 조사하고 그들이 어떤 형태로 진행고
nomal-dev.tistory.com
이 글이 꼭 정답은 아닙니다. 잘못된 부분이나 부족한 부분을 알려주시면 학습 후 수정하겠습니다.
'Flutter > study' 카테고리의 다른 글
모바일 환경에서 무한스크롤을 사용하는 이유가 뭘까? feat. cursor based pagination #1 (0) 2024.02.12 [Flutter] '상태'관리는 어떻게 해야할까요? feat. sealed class (4) 2024.01.24