[팝콘]개발기록 - 멋진 사용자의 토큰 관리해보기

sunghun kim·2025년 3월 17일

[팝콘-프로젝트]

목록 보기
4/6

토큰이 뭔데요?

스크린샷 2025-01-14 11.52.15.png

토큰을 검색하면 동전모양으로, 무언가(서비스 등)와 교환할 수 있는 화폐같은게 나온다.
카지노에서는 칩에 해당하는 용어…이지만 지금 알고싶은 정보는 이게 아니다.


프로그래밍 언어에서 토큰은 컴파일러가 소스코드를 분석할 때 사용되는 가장 작은 의미 단위라고 한다.

int x = 10;

예를 들어보면 int, x, =, 10, ; 모두 하나의 토큰이라고 하는것이다.


그렇지만, 내가 지금 궁금한 것은 네트워킹에서 인증 및 보안과 관련된 토큰의 의미이다.

보통 IT에서 토큰이라고 하면 웹 등에서 개인을 확인하기 위한 수단의 하나로 시스템 간 통신에서 사용자 인증 정보를 포함한 작은 데이터 조각을 말한다고 한다.

토큰의 의미가 무언가를 식별하기 위한 용도로 쓰인다는 것은 분야가 무엇이 되든 같은 의미인거 같다.


토큰의 종류/분류

가볍게 봐도 되며 JWTOAuth토큰은 조금 자세히 보자.

1. 프로그래밍 언어

토큰은 프로그래밍 언어의 문법 요소로 분류된다.
아래와 같이 분류되지만 지금 궁금하진 않다.

  • 키워드(Keywords): int, if, return 등 언어에서 예약된 단어
  • 식별자(Identifiers): 변수명, 함수명 등 사용자가 정의한 이름
  • 연산자(Operators): +, -, *, /, =, == 등 연산을 수행하는 기호
  • 구분자(Delimiters): ;, {, }, () 등 코드를 구분하는 기호
  • 리터럴(Literals): 숫자(10), 문자열("hello") 등 고정된 값
  • 주석(Comment): 프로그램 실행에 영향을 미치지 않는 설명 문구

2. 인증 및 보안

(1) 인증 토큰 종류

  • 세션 토큰(Session Token):
    • 사용자가 로그인한 세션(연결상태)을 유지하기 위한 토큰
    • 서버에 저장되어 세션 관리와 함께 사용
  • JWT(JSON Web Token):
    • 서버와 클라이언트 간 인증 정보를 담은 JSON 기반 토큰
    • 세션이 필요 없으며 주로 웹 애플리케이션에서 사용
  • OAuth 토큰:
    • 권한 위임을 위해 사용
    • 예: Google 로그인이나 Facebook 로그인을 통해 외부 서비스에 액세스 권한을 부여
    • 두 가지로 구분:
      • Access Token: API 요청에 사용
      • Refresh Token: Access Token 만료 시 새로 발급받는 데 사용

(2) CSRF 토큰

(3) 비밀 키 또는 API 토큰

  • 특정 서비스와의 통신에서 인증을 위해 사용
  • 예: API 호출 시 사용하는 개인 키

여기서의 종류는 배척개념이 아니다.
즉, JWT 토큰이라고 해서 OAuth토큰이 아닌것은 아니고 Access랑 Refresh를 사용했다고 해서 OAuth 토큰인 것은 아닌 것이다.
Access와 Refresh 토큰은 권한 부여를 구현하는 일반적인 패턴으로도 많이 사용된다고 한다.

3. 금융 및 블록체인

암호화폐 토큰 종류

(a) 기능에 따른 분류

  1. 유틸리티 토큰(Utility Token):
    • 특정 플랫폼에서 서비스나 상품을 사용하는 데 활용
    • 예: 파일 저장 서비스를 제공하는 Filecoin의 FIL
  2. 보안 토큰(Security Token):
    • 주식, 부동산 등 실물 자산의 소유권을 나타냄
    • 법적 규제를 따름
  3. 결제 토큰(Payment Token):
    • 디지털 화폐로 거래 및 결제 수단
    • 예: 비트코인(BTC), 라이트코인(LTC)
  4. 거버넌스 토큰(Governance Token):
    • 블록체인 네트워크의 의사 결정 과정에 참여할 권리를 부여
    • 예: Uniswap(UNI)

(b) 기술적 분류

  1. 코인(Coin):
    • 자체 블록체인을 보유한 암호화폐
    • 예: 비트코인, 이더리움
  2. 토큰(Token):
    • 타 블록체인의 네트워크를 기반으로 생성된 디지털 자산
    • 예: ERC-20(이더리움), BEP-20(바이낸스 스마트 체인)

4. 기타

(1) 게임 및 서비스

  • 아이템 토큰:
    • 특정 서비스나 게임에서 사용되는 가상의 교환권
    • 예: 게임 내에서 사용하는 코인 또는 티켓

(2) 물리적 토큰

  • 보안 하드웨어 토큰:
    • 보안 인증을 위해 사용하는 물리 장치
    • 예: OTP 생성기, USB 형태의 보안 키(YubiKey)

JWT

https://jwt.io/introduction

JWT(JSON Web Token)은 JSON 포맷을 기반으로 한 압축된 토큰으로, 주로 권한 부여 및 정보 교환 목적으로 사용된다고 한다.


구조

JWT는 .(점)으로 구분된 세 가지 파트로 구성된다.

  1. Header:
    • 토큰의 해싱 알고리즘과 타입(JWT)을 정의하는 내용이 있다.
    • 예:
      {
        "alg": "HS256",
        "typ": "JWT"
      }
  2. Payload:
    • 토큰에 포함될 클레임(Claims)을 담고 있으며, 클레임은 사용자 또는 추가 정보를 나타낸다.
    • 예:
      {
        "sub": "1234567890",
        "name": "John Doe",
        "admin": true,
        "iat": 1516239022
      }
    • 주요 클레임 종류:
      • 등록된 클레임(Registered Claims): 표준화된 필드
        • iss (issuer): 토큰 발급자
        • sub (subject): 토큰의 주체
        • aud (audience): 토큰의 대상
        • exp (expiration): 토큰 만료 시간
        • iat (issued at): 토큰 발급 시간
      • 공개 클레임(Public Claims): 정의된 규약 없이 사용할 수 있는 필드
      • 비공개 클레임(Private Claims): 발급자 간에 합의된 데이터
  3. Signature:
    • 토큰의 무결성을 검증하기 위해 사용되고, 서명은 다음과 같이 생성된다고 한다.
      HMACSHA256(
        base64UrlEncode(header) + "." +
        base64UrlEncode(payload),
        secret
      )

전체 구조

JWT는 다음과 같은 형태를 띄어서 Header, Payload, Signature로 구분된다.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
  • Header: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
  • Payload: eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
  • Signature: SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

특징

  • 무상태성 (Stateless):
    • JWT는 모든 정보를 자체적으로 포함하고 있으므로 서버는 토큰 상태를 저장할 필요가 없다.
    • 토큰을 받은 클라이언트는 요청 시 이 토큰을 포함하여 서버로 보낸다.
  • 보안성:
    • 토큰의 무결성은 서명(Signature)을 통해 보장된다고 한다.
    • 서명은 변경 방지 기능을 제공하지만 암호화는 기본적으로 제공되지 않아서 민감한 정보를 포함해서는 안된다고 한다.
  • 확장성:
    • JSON 기반이라 읽기 쉽고 다양한 클레임과 정보를 포함할 수 있다.
  • 유효성 검증:
    • 서버는 서명과 만료 시간(exp)을 통해 토큰의 유효성을 검증할 수 있다.

동작 방식

1. 사용자 로그인 (JWT 발급)

  • 사용자가 사용자 이름과 비밀번호로 로그인 요청을 보낸다.
  • (인증)서버가 사용자의 자격 증명을 확인한 후 JWT를 생성
    • Access Token: 주로 짧은 기간 동안 리소스 서버에 접근하기 위해 사용
    • Refresh Token: Access Token이 만료된 후 새 Access Token을 발급받기 위해 사용

2. Access Token 사용

  • 클라이언트는 서버의 API에 요청을 보낼 때 JWT Access Token을 포함하여 보낸다.
    • 예: HTTP Authorization 헤더에 포함
      Authorization: Bearer <Access Token>

3. Access Token 검증

  • 서버는 Access Token의 서명을 확인하여 무결성을 검증
    • if 검증이 성공:
      요청을 처리한다.
    • else Access Token 만료:
      Access Token은 보안을 위해 짧은 유효 기간(예: 15분)을 가지며
      만료된 Access Token을 계속 사용하려 하면 서버는 요청을 거부한다.
      - Refresh Token 검증
      - if 검증이 성공:
      ****새로운 Access Token을 요청한다. (reissue)
              ```
              POST /auth/refresh-token
              Authorization: Bearer <Refresh Token>
              ```
              
          - `else` **Refresh Token 만료:
          새로 로그인**하여 Access토큰과 Refresh토큰을 발급받는다.

Access Token과 Refresh Token의 역할

1. Access Token

  • 역할:
    • 사용자가 인증된 상태에서 리소스 서버에 접근할 수 있도록 인증 정보를 제공
    • 짧은 유효 기간(예: 5분 ~ 15분)을 가짐
  • 특징:
    • JWT 형식일 경우 토큰 자체에 사용자 정보와 권한을 포함
    • 클라이언트가 각 요청에 포함하여 사용
    • 유효 기간이 짧아 탈취되더라도 악용 가능성이 낮음

2. Refresh Token

  • 역할:
    • Access Token이 만료된 경우 새로운 Access Token을 발급받는 데 사용
    • 보통 유효 기간이 길며(몇 시간 ~ 며칠) 서버에서만 검증 가능하도록 설계
  • 특징:
    • 클라이언트가 저장하고 Access Token이 만료되었을 때만 서버로 전송
    • JWT 형식이 아닌 고유 식별자로 구현되기도 함
    • 탈취 시 피해가 클 수 있으므로 보안이 중요
      • Refresh Token은 서버와 안전한 통신 채널(예: HTTPS)에서만 사용해야 함

동작 예시

1. 사용자 로그인

POST /auth/login
Content-Type: application/json

{
  "username": "john",
  "password": "mypassword"
}

응답:

{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "refresh_token": "d2hhdCBhIG5pY2UgZGF5IQ=="
}
  • 클라이언트는 Access Token과 Refresh Token을 받아 저장

2. API 요청

GET /api/user-profile
Authorization: Bearer <Access Token>

서버 동작:

  • Access Token의 서명을 검증
  • Payload에 포함된 사용자 정보와 권한 확인
  • 요청 처리 후 응답 반환

3. Access Token 만료

  • 만료된 Access Token으로 API 요청 시 서버는 401 Unauthorized 응답을 반환
HTTP/1.1 401 Unauthorized
Content-Type: application/json

{
  "error": "Access token expired"
}

Access Token과 Refresh Token의 보안 고려사항

  1. Access Token:
    • 짧은 유효 기간으로 설정하여 탈취 시 피해를 줄임
    • HTTP 요청에만 사용하고 민감한 데이터는 포함하지 않음
  2. Refresh Token:
    • 길게 설정되지만 탈취 시 위험하므로 안전하게 저장
    • 브라우저에서는 쿠키(HTTP Only, Secure 옵션 사용)를 사용해 저장
    • 모바일 앱에서는 보안 스토리지(Secure Storage)를 활용
  3. HTTPS:
    • 모든 통신에 HTTPS를 사용하여 토큰 탈취를 방지
  4. Revoke 기능:
    • Refresh Token이 유출되었을 때 무효화(revocation) 기능 필요

Access Token과 Refresh Token의 조합을 사용하는 이유

  • 보안:
    • Access Token은 유효 기간이 짧아 탈취되더라도 피해가 제한적
    • Refresh Token은 서버에서만 검증하므로 추가적인 보호 계층 제공
  • 성능:
    • Access Token은 서버의 상태를 저장하지 않아 요청 속도를 높임
  • 확장성:
    • Stateless 인증으로 서버 확장에 유리

OAuth토큰

https://developers.google.com/identity/protocols/oauth2?hl=ko

OAuth(Open Authorization)는 인터넷 사용자 권한 위임 프로토콜
하나의 애플리케이션이 다른 애플리케이션의 리소스에 사용자의 비밀번호 없이 안전하게 접근할 수 있도록 권한을 부여하는 방식을 제공한다고 한다.

사용자의 비밀번호같은 자격증명을 노출하지 않고
제3자가 특정 리소스에 제한적인 접근 권한을 얻도록 허용하는 것이다.

Google 계정을 사용해 다른 서비스에 로그인하는 소셜로그인, Kakao 계정을 사용해 친구 목록 가져오기 등 이러한 예시를 들 수 있다.


동작방식

OAuth는 클라이언트, 리소스 소유자, 리소스 서버, 인증 서버 이렇게 4개의 상호작용으로 동작한다.

  1. 리소스 소유자:

    리소스를 소유한 사용자 (예: Kakao 계정의 사용자)

  2. 클라이언트:

    리소스에 접근하려는 애플리케이션 (예: 우리가 만드는 팝콘 앱)

  3. 리소스 서버:

    보호된 리소스를 호스팅하는 서버 (예: Kakao API)

  4. 인증 서버:

    리소스 소유자의 인증을 처리하고 권한 부여 토큰을 발급하는 서버 (예: Kakao의 OAuth 서버)


주요 개념

  1. Access Token:
    • 클라이언트가 리소스 서버에 접근하기 위해 사용하는 토큰
    • 유효 기간이 짧으며 리소스 서버는 Access Token을 검증하여 요청을 처리
  2. Refresh Token:
    • Access Token이 만료되었을 때 새로운 Access Token을 발급받는 데 사용
    • 유효 기간이 더 길며 보통 인증 서버에서만 검증
  3. 스코프(Scope):
    • 클라이언트가 접근하려는 리소스와 권한의 범위를 정의
    • 예: email, profile, read:user

OAuth 2.0의 인증 흐름

1. Authorization Code Flow

  • 주로 서버 기반 애플리케이션에서 사용
  • 사용자는 클라이언트를 통해 인증 요청
  • 인증 서버는 사용자 인증 후 클라이언트에 Authorization Code 발급
  • 클라이언트는 Authorization Code로 Access Token 요청
  • 인증 서버는 Access Token을 발급

2. Implicit Flow

  • 주로 브라우저 기반 애플리케이션에서 사용
  • Authorization Code를 생략하고 클라이언트가 Access Token을 직접 얻음
  • 보안 이슈로 인해 권장되지 않는다고 함

3. Resource Owner Password Credentials Flow

  • 보안성이 낮아 신뢰할 수 있는 클라이언트에서만 사용
  • 사용자가 자신의 자격 증명(예: 사용자명, 비밀번호)을 클라이언트에 직접 제공

4. Client Credentials Flow

  • 사용자가 없는 머신 간 통신(서버 간 API 호출)에 사용
  • 클라이언트가 자신의 자격 증명으로 인증 서버에 Access Token 요청

OAuth의 동작 예시

Google 계정으로 로그인

  1. 사용자가 웹사이트에서 "Google로 로그인" 버튼 클릭
  2. 사용자가 Google 로그인 페이지로 리다이렉트
  3. 사용자는 자신의 Google 계정으로 로그인 후 권한 요청에 동의
  4. Google이 인증 후 Authorization Code 발급
  5. 클라이언트는 Authorization Code로 Access Token 요청
  6. Google이 Access Token 발급
  7. 클라이언트는 Access Token을 사용해 Google API 호출(예: 사용자 이메일, 프로필 정보 가져오기)

OAuth의 장점

  1. 비밀번호 보호:
    • 사용자 비밀번호가 제3자 애플리케이션에 노출되지 않음
  2. 세분화된 권한:
    • 스코프를 통해 접근 권한을 세분화 가능
  3. 탈중앙화:
    • 인증과 리소스 접근을 분리하여 다양한 서비스와 쉽게 통합

UserDefaults와 Keychain

iOS에서 데이터 저장소에는 UserDefaultsKeychain이 있다.
각각 일반 데이터민감한 데이터를 저장하는 용도로 사용되며 목적과 보안을 고려해서 사용하면 된다.

멋도모르고 토큰을 UserDefaults에 저장하는 코드를 작성했는데
토큰은 민감한 정보이므로 Keychain에 저장하는 코드로 바꾸어야 한다.


UserDefaults

앱 내에서 사용자 설정이나 상태 정보 등의 간단한 데이터를 영구적으로 저장할 때 사용한다.

  • 문자열, 숫자, 불리언, 배열, 딕셔너리 등 간단한 데이터 유형을 지원하며
    복잡한 객체는 JSON으로 변환하여 저장할 수 있다.
  • iOS 디바이스의 앱 샌드박스 디렉토리 (Library/Preferences)에 저장된다고 한다.
    앱별로 분리되며 다른 앱과 데이터 공유는 불가능하다.
  • 암호화가 되어있지 않으므로 민감한 데이터는 저장하면 안되고,
    작은 데이터 저장에 적합하다.
  • 예를들면 사용자 환경설정(다크모드 여부), 마지막 로그인 날짜, 간단한 앱 상태 데이터(사용자 이름)
    이런 것들이 적합하다.
// 저장
UserDefaults.standard.set("John Doe", forKey: "username")

// 읽기
let username = UserDefaults.standard.string(forKey: "username")

Keychain

iOS와 macOS에 내장된 보안 프레임워크를 통해 구현되며
민감한 데이터(비밀번호, 인증 토큰 등)를 안전하게 저장하기 위한 보안 저장소이다.

  • 문자열(예: 비밀번호), 인증 정보(예: 토큰), 키/인증서 등을 저장한다.
  • Touch ID, Face ID 그리고 암호로 보호할 수 있으며
    저장된 데이터는 암호화되고 iOS 보안 정책을 따른다고 한다.
  • Keychain Sharing을 활성화하면 동일 개발자 계정 내의 여러 앱에서 데이터를 공유할 수 있다고 한다.
  • 저장위치는 iOS가 관리하며 암호화된 파일 시스템에 저장된다.
  • 민감한 데이터를 저장하기 위해 설계되었으므로 많은 양의 데이터 저장은 적합하지 않다.
import Security

func saveToKeychain(service: String, account: String, data: Data) {
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrService as String: service,
        kSecAttrAccount as String: account,
        kSecValueData as String: data
    ]
    SecItemAdd(query as CFDictionary, nil)
}

func getFromKeychain(service: String, account: String) -> Data? {
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrService as String: service,
        kSecAttrAccount as String: account,
        kSecReturnData as String: true,
        kSecMatchLimit as String: kSecMatchLimitOne
    ]
    var dataTypeRef: AnyObject?
    if SecItemCopyMatching(query as CFDictionary, &dataTypeRef) == errSecSuccess {
        return dataTypeRef as? Data
    }
    return nil
}

토큰을 프로젝트에 적용해보자

파일구조와 간단한 설명

스크린샷 2025-01-16 12.03.18.png

스크린샷 2025-01-16 12.02.09.png

스크린샷 2025-01-16 12.04.18.png

스크린샷 2025-01-16 12.04.33.png

  1. SceneDelegate:
    사용자가 앱을 실행시켰을 때 첫번째 뷰를 띄워야 하는 상황이다.
    키체인에 저장된 토큰의 상태에 따라 메인일지 로그인화면일지 나눠지는 UI관련 라이프 사이클이므로
    씬델에 작성했다.
  2. 토큰과 키체인:
    • 토큰: Access, Refresh토큰을 저장하며 각각의 만료일도 저장하게끔 구조체로 정의하였다.
    • 토큰레포: 키체인 매니저를 통하여 토큰을 저장, 업데이트, 가져오기, 삭제하기, Access토큰 재발급의 메서드를 정의해두었다.
    • 토큰 만료 해결자: 토큰레포의 객체를 만들고, 토큰의 상태(Access, Refresh토큰유무)를 확인하여 handleTokenExpiration 메서드를 정의하였다.
    1. Access토큰이 유효하면 completion(true),
    2. Access토큰이 유효하지 않으면
      2.1 Refresh토큰이 유효하면 새로운 Access토큰 발급
      2.2 Refresh토큰이 유효하지 completion(false)
    • 키체인: 키체인으로부터 가져오고, 더하고, 업데이트하고, 삭제하는 메서드를 정의해두었다.
  3. 로그인:
    • 로그인 매니저: 서버와 통신하여 로그인의 성공유무를 알 수 있는 메서드를 정의했다.
      자료형이 토큰구조체인 data도 받는다.
    • loginButtonTapped(): 로그인 매니저 객체를 통해 로그인을하며 실패하면 에러, 성공하면 토큰을 저장한다.
  4. DateFormatter:
    • 로그인이나 reissue메서드에서 받아올 토큰만료일을 처리할 dateFormatter를 정의했다.

고민거리: reissue메서드의 위치

reissue메서드는 Access토큰은 만료이고 Refresh토큰은 만료가 아닐 때 Access토큰을 재발급 받는 메서드이다.

reissue메서드가 TokenExpireResolver 의 메서드에 위치해야할지, TokenRepository 의 메서드에 위치해야할지 고민이 있었다.

결론적으로는 아래의 이유에 따라 TokenRepository 의 메서드에 위치하게 되었다.


TokenRepository가 reissueAccessToken 메서드를 가질 때의 장점

TokenRepositoryKeychain 과의 상호작용을 담당하고 있으며 토큰과 관련된 저장, 조회, 삭제도 포함하고 있다.

장점

  1. 책임 분리:

    TokenRepository는 토큰 데이터를 관리하는 역할을 맡고 있으며 토큰 갱신도 데이터 관리에 속한다고 간주할 수 있다.

  2. 재사용성:

    TokenRepository를 통해 다양한 곳에서 토큰 갱신 로직을 호출할 수 있다.

  3. 코드 구조:

    토큰의 저장, 삭제, 갱신 로직이 한 곳에 집중되어 있어 코드 탐색이 쉬워진다.


TokenExpireResolver가 reissueAccessToken 메서드를 가질 때의 장점

TokenExpireResolver는 주로 토큰 만료 상태를 확인하고 처리하는 데 초점이 맞춰져있다.
만약 reissueAccessToken 로직이 여기 있다면 만료된 토큰의 처리 로직과 갱신 로직이 한 곳에 모일 수 있다는 장점이 있다.

장점

  1. 토큰 만료 처리 집중:

    만료된 토큰과 관련된 모든 작업(갱신 포함)이 TokenExpireResolver에서 처리된다.

  2. 로직 분리:

    TokenRepository는 데이터 관리에 집중하고 TokenExpireResolver는 만료 및 갱신 로직을 관리하도록 역할을 분리할 수 있다.


TokenRepository에 reissueAccessToken을 두고, TokenExpireResolver가 호출

내가 생각한 가장 깔끔한 설계는 다음과 같다

  1. TokenRepository:

    토큰의 저장, 조회, 삭제 및 갱신 요청(서버 통신 포함) 역할을 담당

  2. TokenExpireResolver:

    토큰 만료 상태를 확인하고 필요한 경우 TokenRepository.reissueAccessToken을 호출

profile
기죽지않기

0개의 댓글