사전 자원 서버
개념

자원 서버

gabury1고친 사람 github-actions[bot]

자원 서버는 사용자의 데이터를 맡아 두었다가 허락받고 온 요청에만 그것을 내주는 서버입니다. 요청에 딸려 온 토큰을 보고 이 요청이 무엇까지 해도 되는지 판단합니다. 사용자에게 직접 물어보지도, 사용자의 비밀번호를 받지도 않습니다.

쉽고 빠른 이해

무슨 일을 하는 물건인가 — 보호해야 할 데이터를 들고 있다가, 토큰을 내민 요청에만 그것을 내주는 서버입니다. 사진을 보관하는 서버가 인쇄 앱의 요청을 받고 "이 앱은 읽기만 허락받았구나" 하고 사진을 넘겨주는 쪽이 자원 서버입니다.

왜 이렇게 하나 — 이 역할이 없으면 사진을 쓰려는 앱마다 사용자의 아이디와 비밀번호를 직접 받아야 합니다. 그러면 앱 하나가 뚫릴 때 계정 전체가 넘어가고, 허락한 범위를 지켜 줄 곳도 없습니다.

어떻게 도나

  1. 요청 헤더에 실려 온 토큰을 꺼냅니다
  2. 그 토큰이 위조된 것이 아니고 아직 유효 기간 안인지 봅니다
  3. 토큰에 적힌 허락 범위가 이번 요청을 덮으면 데이터를 내주고, 아니면 거절합니다

대가 — 토큰만 보고 판단하므로 토큰이 새어 나가면 그것을 주운 쪽이 그대로 들어옵니다. 사용자가 허락을 거둬들여도, 이미 나간 토큰이 유효 기간 안인 동안은 자원 서버가 그 사실을 모를 수 있습니다. 자기 앱만 자기 데이터를 쓰는 서비스라면 이 구분을 세울 일이 없습니다.

상세

호텔 객실 문은 카드키를 대야 열립니다. 문은 이 사람이 누구인지 모릅니다. 이 카드키가 이 방, 이 기간에 유효한지만 봅니다.

자원 서버가 하는 일이 이 문과 같습니다. 카드키를 만들어 건네는 프런트는 문과 따로 있습니다.

이 절은 그 프런트와 문이 어떻게 나뉘는지부터 봅니다. 그다음 문이 카드키 한 장에서 무엇을 읽어 내는지, 그 카드키가 위조된 것이 아님을 어떻게 아는지를 차례로 따라갑니다.

접근을 위임할 때 서는 네 참여자

한 사람이 자기 데이터를 다른 앱에게 대신 쓰게 해 주는 것을 접근 위임이라고 부릅니다. 이때 네 쪽이 서로 다른 일을 맡습니다. 자원 서버는 그중 데이터를 실제로 들고 있는 쪽입니다.

참여자 맡는 일
자원 소유자 데이터의 주인. 무엇까지 허락할지 정한다
클라이언트 주인 대신 그 데이터를 쓰려는 앱
인가 서버 주인의 허락을 확인하고 토큰을 내준다
자원 서버 데이터를 들고 있다가 토큰을 보고 내준다

자원 서버가 들고 있는 데이터 하나하나를 자원이라고 부릅니다. 그중 허락 없이는 못 가져가게 막아 둔 것이 보호된 자원입니다. 사진 한 장, 주소록 한 건, 결제 내역 한 줄이 각각 그런 자원입니다.

넷이 서로를 어떻게 부르는지, 보호된 자원은 어디에 들어 있는지를 그림으로 보면 이렇습니다.

flowchart TD
    subgraph RS["자원 서버"]
        subgraph RES["자원"]
            P["보호된 자원"]
        end
    end
    O["자원 소유자"] -->|허락| AS["인가 서버"]
    AS -->|토큰| C["클라이언트"]
    C -->|토큰을 실은 요청| RS

허락을 따지는 일과 데이터를 보관하는 일을 다른 쪽에 맡긴 것이 이 구조의 뼈대입니다. 누구에게 무엇을 내줄지 판단하는 인가는 인가 서버가 합니다. 자원 서버는 그 판단 결과를 적은 토큰만 받아 씁니다.

토큰 한 장이 비밀번호를 대신합니다

자원 서버는 요청을 보낸 쪽을 비밀번호로 확인하지 않습니다. 대신 액세스 토큰이라 부르는 짧은 문자열 하나를 받습니다. 이 문자열에는 누구의 데이터를 어디까지 언제까지 만져도 되는지가 담겨 있습니다.

토큰은 요청 헤더에 실려 옵니다.

http
GET /photos/42
Authorization: Bearer eyJhbGciOi...

첫 줄이 42번 사진을 달라는 요청이고, 둘째 줄이 그 요청에 딸린 토큰입니다. Bearer 는 이 토큰을 가진 쪽이면 누구든 받아 준다는 뜻입니다. 그래서 토큰을 주운 쪽도 똑같이 쓸 수 있습니다. 토큰이 오가는 길이 암호화된 연결이어야 하는 까닭입니다.

sequenceDiagram
    participant 앱 as 클라이언트
    participant 인가 as 인가 서버
    participant 자원 as 자원 서버
    앱->>인가: 사용자의 허락을 받아 토큰을 받아 온다
    인가-->>앱: 읽기만 허락된 토큰
    앱->>자원: 그 토큰을 헤더에 실어 사진을 요청한다
    Note over 자원: 이 그림에서는 자원 서버가 혼자 판단한다
    자원-->>앱: 사진

토큰을 만들어 주는 쪽과 그것을 보고 데이터를 내주는 쪽이 갈라져 있습니다. 자원 서버는 사용자가 언제 무엇을 허락했는지 모릅니다. 그 대답이 이미 토큰 안에 들어 있기 때문에 알 필요도 없습니다.

요청 하나를 받으면 무엇을 보나

자원 서버는 요청을 받을 때마다 세 관문을 차례로 지납니다. 토큰이 위조된 것은 아닌지, 아직 유효 기간 안인지, 이 요청이 허락받은 범위 안인지를 봅니다.

세 번째 관문이 보는 허락 범위를 스코프라고 부릅니다. 토큰에 "사진 읽기" 라고 적혀 있으면 사진을 지우는 요청은 그 범위 밖입니다. 읽기만 허락된 토큰으로 삭제를 시키면 자원 서버가 거절합니다.

세 관문을 그림으로 보면 이렇습니다.

flowchart TD
    A["요청이 들어온다"] --> B{"토큰이 진짜인가"}
    B -->|아니다| X1["거절한다 · 토큰이 문제다"]
    B -->|그렇다| C{"아직 유효 기간 안인가"}
    C -->|아니다| X1
    C -->|그렇다| D{"스코프가 이 요청을 덮나"}
    D -->|아니다| X2["거절한다 · 범위가 문제다"]
    D -->|그렇다| E["자원을 내준다"]

앞 관문을 통과해야 다음 관문을 봅니다. 하나라도 걸리면 데이터가 나가지 않습니다. 거절이 두 종류인 것은 앱이 할 일이 갈리기 때문입니다. 토큰이 문제였으면 앱은 토큰을 새로 받아 오면 되고, 범위가 문제였으면 애초에 허락받지 못한 일이라 포기해야 합니다.

필요한 만큼만 내주자는 원칙을 최소 권한이라고 합니다. 누가 무엇에 손댈 수 있는지를 미리 정해 두고 지키는 일은 접근 제어라고 부릅니다. 자원 서버는 스코프가 그은 선을 요청마다 실제로 지켜 내는 쪽입니다.

토큰이 진짜인지 아는 두 방법

위조 여부를 가리는 방법은 크게 둘입니다. 하나는 토큰 안에 든 서명을 자원 서버가 직접 검사하는 것이고, 다른 하나는 인가 서버에 그 토큰이 아직 유효한지 물어보는 것입니다. 앞 그림에서 자원 서버가 인가 서버를 부르지 않은 것이 그중 첫 번째 방법입니다.

서명을 직접 검사 인가 서버에 물어보기
미리 갖춰야 할 것 인가 서버의 공개 키 인가 서버를 부르는 통로
요청 하나에 드는 비용 계산 한 번 네트워크 왕복 한 번
허락이 거둬진 토큰 유효 기간이 끝날 때까지 통과한다 곧바로 막힌다

앞의 방법은 자원 서버 혼자 끝낼 수 있어 부하가 몰려도 버팁니다. 대신 이미 나간 토큰을 중간에 막지 못합니다. 뒤의 방법은 매번 인가 서버를 부르는 만큼 응답이 늦어지지만, 언제나 최신 상태를 봅니다.

두 방법이 갈리는 곳은 하나입니다. 사용자가 허락을 거둬들인 때와 토큰의 유효 기간이 끝나는 때 사이에는 구간이 있고, 그 구간에서 답이 달라집니다.

stateDiagram-v2
    state "유효 · 자원 서버가 통과시킨다" as 유효
    state "허락은 거둬졌지만 유효 기간 안" as 거둬짐
    state "유효 기간이 끝났다 · 양쪽 다 막는다" as 끝남
    [*] --> 유효: 인가 서버가 토큰을 내준다
    유효 --> 거둬짐: 사용자가 허락을 거둬들인다
    유효 --> 끝남: 유효 기간이 지난다
    거둬짐 --> 끝남: 유효 기간이 지난다
    note right of 거둬짐
        서명만 검사하면 그대로 통과한다
        인가 서버에 물어보면 여기서 막힌다
    end note

그래서 둘을 섞어 씁니다. 토큰의 유효 기간을 짧게 잡아 두고 평소에는 서명만 검사하다가, 돈이 오가는 요청처럼 되돌릴 수 없는 일에만 인가 서버에 물어보는 식입니다.

인가 서버와 갈라 둔 까닭

자원 서버와 인가 서버는 같은 프로세스 안에 있어도 됩니다. 서비스가 하나뿐일 때는 로그인도 시켜 주고 데이터도 내주는 서버 한 대가 두 역할을 겸합니다. 가르는 것이 값을 하는 때는 서비스가 여럿이 될 때부터입니다. 두 배치를 나란히 놓으면 이렇습니다.

flowchart TD
    subgraph ONE["서비스가 하나일 때"]
        C1["클라이언트"] -->|토큰| S["서버 한 대 · 인가 서버 역할 + 자원 서버 역할"]
    end
    subgraph MANY["서비스가 여럿일 때"]
        AS2["인가 서버"] -->|토큰 한 장| C2["클라이언트"]
        C2 --> R1["자원 서버 · 사진"]
        C2 --> R2["자원 서버 · 주소록"]
        C2 --> R3["자원 서버 · 결제"]
    end

갈라 놓은 것은 기계가 아니라 역할입니다. 자기 앱만 자기 데이터를 쓰는 곳이라면 이 구분을 따로 세울 이유가 없습니다. 접근 위임은 남의 앱에게 내 데이터를 내줄 때 필요한 장치입니다. 자원 서버라는 이름도 그때 의미를 갖습니다.

토큰을 내주는 쪽을 한 곳으로 모아 두면, 앱은 토큰 한 장으로 여러 API(Application Programming Interface)를 부를 수 있습니다. 각 서비스는 자기 데이터에 대한 판단만 하면 됩니다.

그래서 자원 서버가 하지 않는 일도 분명해집니다. 사용자가 본인이 맞는지 확인하는 인증은 자원 서버의 몫이 아닙니다. 로그인 화면과 동의 화면을 띄우는 일도, 토큰을 만들어 내주는 일도 인가 서버가 합니다. 자원 서버는 받은 토큰을 읽고 문을 열지 말지를 정할 뿐입니다.

관련 항목

자원 서버와 한 무대에 서는 참여자

클라이언트 · 인가 서버 · 자원 소유자 · 사용자 에이전트

자원 서버가 받아 검사하는 토큰

액세스 토큰 · Bearer 토큰 · JWT · 리프레시 토큰 · 토큰

자원 서버가 지키는 대상과 그 범위

자원 · 보호된 자원 · 스코프 · 권한

자원 서버의 판단을 떠받치는 개념

인증 · 인가 · 인증과 인가 · 접근 제어 · 최소 권한

자원 서버를 세워 두는 접근 위임 표준

OAuth 2.0 · OpenID Connect · SAML · API 키

토큰이 진짜인지 확인하는 수단

토큰 인트로스펙션 · 디지털 서명 · 공개 키 · TLS

자원 서버 앞단에서 요청을 먼저 받는 장비

API 게이트웨이 · 게이트웨이 · 리버스 프록시 · 웹 서버

자원 서버의 검사가 뚫릴 때 나는 공격

토큰 탈취 · 재생 공격 · 권한 상승 · 중간자 공격

다른 이름: resource server · 리소스 서버