포스트

[Python 100일 챌린지] Day 57 - API 인증

[Python 100일 챌린지] Day 57 - API 인증

headers={'X-API-Key': 'secret-key'} → 인증 성공! 없으면 401 Unauthorized! 😊

API 키, JWT 토큰, OAuth… 내 API를 보호하는 방법! 아무나 접근 못하는 보안 API를 만듭니다!

(35-45분 완독 ⭐⭐⭐)

🎯 오늘의 학습 목표

📚 사전 지식


🎯 학습 목표 1: API 인증 방식 이해하기

인증(Authentication)이란?

인증은 사용자나 클라이언트가 “누구인지” 확인하는 과정입니다. API에서 인증이 필요한 이유:

이유 설명 예시
무단 접근 방지 허가받지 않은 사용자 차단 회원만 데이터 조회 가능
사용량 추적 사용자별 요청 횟수 제한 하루 1000건 제한
개인화 사용자별 맞춤 데이터 제공 내 프로필, 내 주문 내역
책임 추적 누가 무엇을 했는지 기록 로그에 user_id 저장

주요 인증 방식 비교

방식 보안 수준 구현 난이도 사용 사례 장점 단점
API Key ⭐⭐ 쉬움 간단한 서비스 간 통신 구현 간단, 빠름 탈취 위험, 권한 관리 어려움
Basic Auth ⭐⭐ 쉬움 내부 API, 테스트 HTTP 표준, 간단 매번 비밀번호 전송 위험
Bearer Token ⭐⭐⭐ 보통 대부분의 웹/앱 토큰 만료, 권한 제어 토큰 관리 필요
OAuth 2.0 ⭐⭐⭐⭐ 어려움 제3자 로그인 (구글, 카카오) 비밀번호 공유 안함, 권한 세분화 복잡한 플로우
JWT ⭐⭐⭐⭐ 보통 Stateless API, 마이크로서비스 서버 부담 적음, 확장성 좋음 토큰 크기 큼, 취소 어려움

인증 vs 인가

1
2
3
4
5
6
7
8
9
10
# 인증 (Authentication): "당신은 누구인가?"
# → 로그인 성공 여부 확인
user = authenticate(username, password)

# 인가 (Authorization): "당신은 무엇을 할 수 있는가?"
# → 권한 확인
if user.role == 'admin':
    allow_delete()
else:
    deny_access()

🎯 학습 목표 2: API 키 인증 사용하기

API 키란?

API 키는 클라이언트를 식별하는 고유한 문자열입니다. 가장 간단한 인증 방식입니다.

기본 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
from flask import Flask, request, jsonify
from functools import wraps
import os

app = Flask(__name__)

# 환경 변수에서 API 키 가져오기 (보안!)
API_KEY = os.getenv('API_KEY', 'development-key-only')

def require_api_key(f):
    """API 키 검증 데코레이터"""
    @wraps(f)
    def decorated_function(*args, **kwargs):
        # 헤더에서 API 키 추출
        api_key = request.headers.get('X-API-Key')

        if not api_key:
            return jsonify({
                'error': 'API key required',
                'message': 'Please provide X-API-Key header'
            }), 401

        if api_key != API_KEY:
            return jsonify({
                'error': 'Invalid API key',
                'message': 'The provided API key is not valid'
            }), 403

        # 인증 성공
        return f(*args, **kwargs)

    return decorated_function

# 보호된 엔드포인트
@app.route('/api/data')
@require_api_key
def get_data():
    return jsonify({
        'data': [1, 2, 3, 4, 5],
        'message': 'Access granted'
    })

# 공개 엔드포인트
@app.route('/api/public')
def public_data():
    return jsonify({'message': 'Anyone can access this'})

사용 예시:

1
2
3
4
5
6
7
8
9
10
11
import requests

# ❌ API 키 없이 요청
response = requests.get('http://localhost:5000/api/data')
print(response.status_code)  # 401

# ✅ API 키와 함께 요청
headers = {'X-API-Key': 'development-key-only'}
response = requests.get('http://localhost:5000/api/data', headers=headers)
print(response.json())
# {'data': [1, 2, 3, 4, 5], 'message': 'Access granted'}

API 키 관리 모범 사례

1. 환경 변수 사용
1
2
3
4
5
6
7
8
9
10
11
# ❌ 코드에 직접 하드코딩 (위험!)
API_KEY = 'sk-abc123xyz'

# ✅ 환경 변수 사용
import os
API_KEY = os.getenv('API_KEY')

# .env 파일 사용 (python-dotenv)
from dotenv import load_dotenv
load_dotenv()
API_KEY = os.getenv('API_KEY')

.env 파일 (절대 Git에 커밋하지 말 것!):

1
API_KEY=sk-production-key-xyz123
2. 데이터베이스에 여러 키 저장
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 여러 사용자에게 각각 다른 API 키 발급
api_keys = {
    'user-001': 'sk-user001-abc',
    'user-002': 'sk-user002-def',
    'admin-001': 'sk-admin001-xyz'
}

def verify_api_key(key):
    for user_id, stored_key in api_keys.items():
        if key == stored_key:
            return user_id  # 키가 유효하면 사용자 ID 반환
    return None

@app.route('/api/data')
def get_data():
    api_key = request.headers.get('X-API-Key')
    user_id = verify_api_key(api_key)

    if not user_id:
        return jsonify({'error': 'Invalid API key'}), 403

    return jsonify({
        'user_id': user_id,
        'data': 'Your data here'
    })
3. 키 해싱 저장
1
2
3
4
5
6
7
8
9
10
11
12
import hashlib

# 키 생성 시: 원본 키를 해시하여 저장
def hash_api_key(key):
    return hashlib.sha256(key.encode()).hexdigest()

# 저장된 해시 (데이터베이스)
stored_hash = hash_api_key('sk-user001-abc')

# 검증 시: 요청된 키를 해시하여 비교
def verify_hashed_key(input_key, stored_hash):
    return hash_api_key(input_key) == stored_hash

장점: 데이터베이스가 유출되어도 원본 키는 안전함

사용량 제한 (Rate Limiting)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
from datetime import datetime, timedelta
from collections import defaultdict

# 사용자별 요청 기록
request_counts = defaultdict(list)

def rate_limit(max_requests=100, window_minutes=60):
    """시간당 요청 횟수 제한 데코레이터"""
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            api_key = request.headers.get('X-API-Key')
            now = datetime.now()
            cutoff = now - timedelta(minutes=window_minutes)

            # 시간 윈도우 내 요청만 유지
            request_counts[api_key] = [
                req_time for req_time in request_counts[api_key]
                if req_time > cutoff
            ]

            # 요청 횟수 확인
            if len(request_counts[api_key]) >= max_requests:
                return jsonify({
                    'error': 'Rate limit exceeded',
                    'message': f'Max {max_requests} requests per {window_minutes} minutes'
                }), 429

            # 현재 요청 기록
            request_counts[api_key].append(now)

            return f(*args, **kwargs)

        return decorated_function
    return decorator

@app.route('/api/data')
@require_api_key
@rate_limit(max_requests=10, window_minutes=1)
def get_data():
    return jsonify({'data': 'success'})

🎯 학습 목표 3: OAuth 2.0 인증 구현하기

OAuth 2.0이란?

OAuth 2.0은 제3자 애플리케이션이 사용자의 비밀번호 없이 제한된 권한으로 리소스에 접근할 수 있게 하는 인가 프레임워크입니다.

OAuth 2.0 주요 개념

용어 설명 예시
Resource Owner 리소스 소유자 (사용자) 당신
Client 접근 권한을 요청하는 애플리케이션 우리가 만드는 앱
Authorization Server 인증 서버 Google, 카카오 로그인 서버
Resource Server 실제 리소스를 가진 서버 Google 캘린더 API
Access Token 리소스 접근을 위한 토큰 ya29.a0AfH6...
Refresh Token Access Token 갱신용 토큰 장기간 유효

OAuth 2.0 Authorization Code Flow

1
2
3
4
5
6
7
8
1. [사용자] → [우리 앱]: "구글로 로그인" 버튼 클릭
2. [우리 앱] → [구글]: "이 사용자 인증해주세요" (client_id 포함)
3. [구글] → [사용자]: "이 앱에 권한을 줄까요?" (동의 화면)
4. [사용자] → [구글]: "동의" 클릭
5. [구글] → [우리 앱]: Authorization Code 발급 (리다이렉트)
6. [우리 앱] → [구글]: "이 코드로 토큰 주세요" (client_secret 포함)
7. [구글] → [우리 앱]: Access Token + Refresh Token 발급
8. [우리 앱] → [구글 API]: Access Token으로 리소스 요청

간단한 OAuth 2.0 구현 (GitHub 로그인 예시)

1. GitHub OAuth App 등록
  1. GitHub → Settings → Developer settings → OAuth Apps
  2. New OAuth App 클릭
  3. 정보 입력:
    • Application name: My App
    • Homepage URL: http://localhost:5000
    • Authorization callback URL: http://localhost:5000/callback
  4. Client ID와 Client Secret 확인
2. Flask 앱 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
from flask import Flask, redirect, request, session, jsonify
import requests
import os

app = Flask(__name__)
app.secret_key = 'super-secret-key'

# GitHub OAuth 설정
GITHUB_CLIENT_ID = os.getenv('GITHUB_CLIENT_ID')
GITHUB_CLIENT_SECRET = os.getenv('GITHUB_CLIENT_SECRET')
GITHUB_AUTHORIZE_URL = 'https://github.com/login/oauth/authorize'
GITHUB_TOKEN_URL = 'https://github.com/login/oauth/access_token'
GITHUB_API_URL = 'https://api.github.com/user'

@app.route('/')
def home():
    return '''
    <h1>OAuth 2.0 Example</h1>
    <a href="/login">Login with GitHub</a>
    '''

@app.route('/login')
def login():
    """1단계: GitHub 인증 페이지로 리다이렉트"""
    params = {
        'client_id': GITHUB_CLIENT_ID,
        'redirect_uri': 'http://localhost:5000/callback',
        'scope': 'read:user user:email'
    }

    url = f"{GITHUB_AUTHORIZE_URL}?client_id={params['client_id']}&redirect_uri={params['redirect_uri']}&scope={params['scope']}"

    return redirect(url)

@app.route('/callback')
def callback():
    """2단계: Authorization Code를 Access Token으로 교환"""
    code = request.args.get('code')

    if not code:
        return 'Error: No code provided', 400

    # Access Token 요청
    token_response = requests.post(
        GITHUB_TOKEN_URL,
        headers={'Accept': 'application/json'},
        data={
            'client_id': GITHUB_CLIENT_ID,
            'client_secret': GITHUB_CLIENT_SECRET,
            'code': code
        }
    )

    token_data = token_response.json()
    access_token = token_data.get('access_token')

    if not access_token:
        return 'Error: Failed to get access token', 400

    # Access Token으로 사용자 정보 요청
    user_response = requests.get(
        GITHUB_API_URL,
        headers={'Authorization': f'Bearer {access_token}'}
    )

    user_data = user_response.json()

    # 세션에 사용자 정보 저장
    session['user'] = {
        'username': user_data.get('login'),
        'name': user_data.get('name'),
        'email': user_data.get('email')
    }

    return redirect('/profile')

@app.route('/profile')
def profile():
    """로그인된 사용자 프로필"""
    user = session.get('user')

    if not user:
        return redirect('/login')

    return jsonify({
        'message': 'Logged in successfully',
        'user': user
    })

@app.route('/logout')
def logout():
    session.pop('user', None)
    return redirect('/')

OAuth 2.0 장점

  • 비밀번호 공유 안 함: 제3자 앱에 비밀번호 노출 없음
  • 제한된 권한: 필요한 권한만 요청 가능 (scope)
  • 토큰 만료: Access Token은 짧은 시간 후 만료
  • 토큰 취소: 사용자가 언제든지 권한 철회 가능

🎯 학습 목표 4: JWT 토큰 인증 활용하기

JWT (JSON Web Token)란?

JWT는 JSON 형식의 자가 포함 토큰 (Self-contained Token)입니다. 토큰 자체에 사용자 정보와 권한이 포함되어 있어, 서버가 세션을 저장할 필요가 없습니다 (Stateless).

JWT 구조

JWT는 .으로 구분된 3개 부분으로 구성됩니다:

1
2
3
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
│                                   │                                                                │
│          Header                  │                    Payload                                      │         Signature
부분 내용 예시
Header 알고리즘, 토큰 타입 {"alg": "HS256", "typ": "JWT"}
Payload 사용자 정보, 권한, 만료 시간 {"sub": "user123", "role": "admin", "exp": 1735689600}
Signature 위변조 방지 서명 HMACSHA256(base64(header) + "." + base64(payload), secret)

JWT 생성과 검증

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
import jwt
from datetime import datetime, timedelta
from flask import Flask, request, jsonify
from functools import wraps

app = Flask(__name__)

# JWT 비밀 키 (절대 공개하지 말 것!)
SECRET_KEY = 'your-secret-key-keep-it-safe'

def create_jwt_token(user_id, role='user', expires_in_hours=24):
    """JWT 토큰 생성"""
    payload = {
        'sub': user_id,  # Subject (사용자 ID)
        'role': role,
        'iat': datetime.utcnow(),  # Issued At (발급 시각)
        'exp': datetime.utcnow() + timedelta(hours=expires_in_hours)  # Expiration
    }

    token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')
    return token

def verify_jwt_token(token):
    """JWT 토큰 검증"""
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
        return payload
    except jwt.ExpiredSignatureError:
        return None  # 만료된 토큰
    except jwt.InvalidTokenError:
        return None  # 유효하지 않은 토큰

# 로그인 엔드포인트
@app.route('/api/login', methods=['POST'])
def login():
    """사용자 로그인"""
    data = request.json
    username = data.get('username')
    password = data.get('password')

    # 실제로는 데이터베이스에서 사용자 확인
    if username == 'admin' and password == 'password123':
        token = create_jwt_token(user_id='user-001', role='admin')

        return jsonify({
            'message': 'Login successful',
            'token': token
        })
    else:
        return jsonify({'error': 'Invalid credentials'}), 401

# JWT 인증 데코레이터
def require_jwt(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        # Authorization 헤더에서 토큰 추출
        auth_header = request.headers.get('Authorization')

        if not auth_header:
            return jsonify({'error': 'No token provided'}), 401

        # "Bearer <token>" 형식에서 토큰만 추출
        try:
            token = auth_header.split(' ')[1]
        except IndexError:
            return jsonify({'error': 'Invalid token format'}), 401

        # 토큰 검증
        payload = verify_jwt_token(token)

        if not payload:
            return jsonify({'error': 'Invalid or expired token'}), 401

        # request에 사용자 정보 추가
        request.user = payload

        return f(*args, **kwargs)

    return decorated_function

# 보호된 엔드포인트
@app.route('/api/profile')
@require_jwt
def profile():
    return jsonify({
        'user_id': request.user['sub'],
        'role': request.user['role'],
        'message': 'This is your profile'
    })

# 관리자 전용 엔드포인트
@app.route('/api/admin')
@require_jwt
def admin_only():
    if request.user['role'] != 'admin':
        return jsonify({'error': 'Admin access required'}), 403

    return jsonify({'message': 'Welcome, admin!'})

사용 예시:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import requests

# 1. 로그인하여 토큰 받기
response = requests.post('http://localhost:5000/api/login', json={
    'username': 'admin',
    'password': 'password123'
})

token = response.json()['token']
print(f"Token: {token}")

# 2. 토큰으로 보호된 엔드포인트 접근
headers = {'Authorization': f'Bearer {token}'}

profile = requests.get('http://localhost:5000/api/profile', headers=headers)
print(profile.json())
# {'user_id': 'user-001', 'role': 'admin', 'message': 'This is your profile'}

admin_data = requests.get('http://localhost:5000/api/admin', headers=headers)
print(admin_data.json())
# {'message': 'Welcome, admin!'}

JWT Payload Claims

Claim 설명 예시
sub Subject (사용자 ID) "user-123"
iat Issued At (발급 시각) 1640000000
exp Expiration (만료 시각) 1640086400
iss Issuer (발급자) "myapp.com"
aud Audience (대상) "api.myapp.com"
role 사용자 역할 (커스텀) "admin"
email 이메일 (커스텀) "user@example.com"

JWT 장단점

장점 ✅:

  • Stateless: 서버가 세션을 저장할 필요 없음 → 확장성 좋음
  • 자가 포함: 토큰에 모든 정보가 있어 DB 조회 불필요
  • 분산 시스템 친화적: 마이크로서비스 아키텍처에 적합

단점 ⚠️:

  • 토큰 크기: 세션 ID보다 크기가 큼 (네트워크 부담)
  • 취소 어려움: 만료 전까지 유효함 (블랙리스트 필요)
  • 보안 주의: 비밀 키 유출 시 모든 토큰 무효화

💡 실전 팁 & 주의사항

✅ DO (이렇게 하세요)

  • 환경 변수 사용: API 키, 시크릿은 절대 코드에 하드코딩하지 말고 환경 변수로 관리
  • HTTPS 필수: 인증 정보는 반드시 암호화된 연결(HTTPS)로 전송
  • 토큰 만료 설정: Access Token은 짧게(15분~1시간), Refresh Token은 길게(7일~30일)
  • 비밀번호 해싱: 비밀번호는 bcrypt, Argon2 등으로 해싱하여 저장
  • 사용량 제한 (Rate Limiting): 무차별 대입 공격 방지를 위해 요청 횟수 제한
  • CORS 설정: 허용된 도메인만 API 접근 가능하도록 설정

❌ DON’T (하지 마세요)

  • API 키를 Git에 커밋: .env 파일은 반드시 .gitignore에 추가
  • GET 요청에 민감한 정보: 로그인 정보를 URL 파라미터로 전송하지 말 것 (로그에 남음)
  • JWT에 민감한 정보 저장: JWT는 디코딩 가능 → 비밀번호, 카드 번호 등 넣지 말 것
  • 같은 API 키를 모든 사용자에게 공유: 사용자별로 고유한 키 발급
  • 토큰을 localStorage에 저장: XSS 공격에 취약 → httpOnly 쿠키 사용 권장

🔐 비밀번호 해싱 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from werkzeug.security import generate_password_hash, check_password_hash

# 회원가입 시: 비밀번호 해싱
password = 'user-password-123'
hashed_password = generate_password_hash(password)

# 데이터베이스에 저장 (해시만 저장!)
users = {
    'user-001': {
        'username': 'alice',
        'password_hash': hashed_password  # 원본 비밀번호는 저장 안 함!
    }
}

# 로그인 시: 비밀번호 검증
input_password = 'user-password-123'
stored_hash = users['user-001']['password_hash']

if check_password_hash(stored_hash, input_password):
    print("✅ 로그인 성공")
else:
    print("❌ 비밀번호 틀림")

🧪 연습 문제

문제 1: 보호된 TODO API 만들기

요구사항:

  1. API 키 인증을 사용하는 TODO API 작성
  2. /api/todos (GET): 모든 TODO 조회
  3. /api/todos (POST): 새 TODO 추가
  4. API 키가 없으면 401 에러 반환
  5. 사용자별로 다른 API 키 사용 (최소 2명)
💡 힌트
  • require_api_key 데코레이터 재사용
  • 사용자별 API 키를 딕셔너리로 저장
  • 각 사용자의 TODO 리스트를 분리하여 관리
✅ 정답 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
from flask import Flask, request, jsonify
from functools import wraps

app = Flask(__name__)

# 사용자별 API 키
API_KEYS = {
    'sk-user001-abc': 'user-001',
    'sk-user002-def': 'user-002'
}

# 사용자별 TODO 저장소
todos_db = {
    'user-001': [
        {'id': 1, 'title': 'Buy groceries', 'done': False}
    ],
    'user-002': [
        {'id': 1, 'title': 'Finish homework', 'done': False}
    ]
}

def require_api_key(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        api_key = request.headers.get('X-API-Key')

        if not api_key or api_key not in API_KEYS:
            return jsonify({'error': 'Invalid or missing API key'}), 401

        # 사용자 ID를 request에 추가
        request.user_id = API_KEYS[api_key]

        return f(*args, **kwargs)

    return decorated_function

@app.route('/api/todos', methods=['GET'])
@require_api_key
def get_todos():
    """사용자의 TODO 목록 조회"""
    user_todos = todos_db.get(request.user_id, [])

    return jsonify({
        'user_id': request.user_id,
        'todos': user_todos,
        'count': len(user_todos)
    })

@app.route('/api/todos', methods=['POST'])
@require_api_key
def create_todo():
    """새 TODO 추가"""
    data = request.json
    title = data.get('title')

    if not title:
        return jsonify({'error': 'Title is required'}), 400

    # 사용자의 TODO 리스트 가져오기
    user_todos = todos_db.get(request.user_id, [])

    # 새 TODO 생성
    new_id = max([t['id'] for t in user_todos], default=0) + 1
    new_todo = {
        'id': new_id,
        'title': title,
        'done': False
    }

    user_todos.append(new_todo)
    todos_db[request.user_id] = user_todos

    return jsonify({
        'message': 'TODO created',
        'todo': new_todo
    }), 201

if __name__ == '__main__':
    app.run(debug=True)

테스트:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import requests

# User 1의 TODO 조회
headers1 = {'X-API-Key': 'sk-user001-abc'}
response = requests.get('http://localhost:5000/api/todos', headers=headers1)
print(response.json())
# {'user_id': 'user-001', 'todos': [...], 'count': 1}

# User 2의 TODO 추가
headers2 = {'X-API-Key': 'sk-user002-def'}
response = requests.post('http://localhost:5000/api/todos',
                        headers=headers2,
                        json={'title': 'Study Python'})
print(response.json())
# {'message': 'TODO created', 'todo': {'id': 2, 'title': 'Study Python', 'done': False}}

# API 키 없이 요청
response = requests.get('http://localhost:5000/api/todos')
print(response.status_code)  # 401

문제 2: JWT 기반 사용자 등록과 로그인

요구사항:

  1. /api/register (POST): 사용자 등록 (username, password)
  2. /api/login (POST): 로그인 → JWT 토큰 발급
  3. /api/me (GET): 현재 로그인한 사용자 정보 조회 (JWT 필요)
  4. 비밀번호는 해싱하여 저장
  5. 토큰 만료 시간은 1시간
💡 힌트
  • werkzeug.securitygenerate_password_hash, check_password_hash 사용
  • jwt.encodejwt.decode 사용
  • 사용자 정보는 딕셔너리로 간단히 저장
✅ 정답 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
from flask import Flask, request, jsonify
from functools import wraps
from werkzeug.security import generate_password_hash, check_password_hash
import jwt
from datetime import datetime, timedelta

app = Flask(__name__)
SECRET_KEY = 'jwt-secret-key-keep-safe'

# 사용자 데이터베이스 (실제로는 DB 사용)
users_db = {}

@app.route('/api/register', methods=['POST'])
def register():
    """사용자 등록"""
    data = request.json
    username = data.get('username')
    password = data.get('password')

    if not username or not password:
        return jsonify({'error': 'Username and password required'}), 400

    if username in users_db:
        return jsonify({'error': 'Username already exists'}), 409

    # 비밀번호 해싱
    password_hash = generate_password_hash(password)

    # 사용자 저장
    users_db[username] = {
        'username': username,
        'password_hash': password_hash,
        'created_at': datetime.now().isoformat()
    }

    return jsonify({
        'message': 'User registered successfully',
        'username': username
    }), 201

@app.route('/api/login', methods=['POST'])
def login():
    """로그인 → JWT 토큰 발급"""
    data = request.json
    username = data.get('username')
    password = data.get('password')

    # 사용자 존재 확인
    user = users_db.get(username)

    if not user:
        return jsonify({'error': 'Invalid credentials'}), 401

    # 비밀번호 검증
    if not check_password_hash(user['password_hash'], password):
        return jsonify({'error': 'Invalid credentials'}), 401

    # JWT 토큰 생성
    payload = {
        'sub': username,
        'iat': datetime.utcnow(),
        'exp': datetime.utcnow() + timedelta(hours=1)
    }

    token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')

    return jsonify({
        'message': 'Login successful',
        'token': token,
        'expires_in': 3600
    })

def require_jwt(f):
    """JWT 인증 데코레이터"""
    @wraps(f)
    def decorated_function(*args, **kwargs):
        auth_header = request.headers.get('Authorization')

        if not auth_header or not auth_header.startswith('Bearer '):
            return jsonify({'error': 'No token provided'}), 401

        token = auth_header.split(' ')[1]

        try:
            payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
            request.username = payload['sub']
        except jwt.ExpiredSignatureError:
            return jsonify({'error': 'Token expired'}), 401
        except jwt.InvalidTokenError:
            return jsonify({'error': 'Invalid token'}), 401

        return f(*args, **kwargs)

    return decorated_function

@app.route('/api/me')
@require_jwt
def get_current_user():
    """현재 로그인한 사용자 정보"""
    user = users_db.get(request.username)

    if not user:
        return jsonify({'error': 'User not found'}), 404

    return jsonify({
        'username': user['username'],
        'created_at': user['created_at']
    })

if __name__ == '__main__':
    app.run(debug=True)

테스트:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import requests

# 1. 사용자 등록
response = requests.post('http://localhost:5000/api/register', json={
    'username': 'alice',
    'password': 'secure-password-123'
})
print(response.json())
# {'message': 'User registered successfully', 'username': 'alice'}

# 2. 로그인
response = requests.post('http://localhost:5000/api/login', json={
    'username': 'alice',
    'password': 'secure-password-123'
})
token = response.json()['token']
print(f"Token: {token[:50]}...")

# 3. 내 정보 조회
headers = {'Authorization': f'Bearer {token}'}
response = requests.get('http://localhost:5000/api/me', headers=headers)
print(response.json())
# {'username': 'alice', 'created_at': '2025-04-26T...'}

# 4. 토큰 없이 요청
response = requests.get('http://localhost:5000/api/me')
print(response.status_code)  # 401

📝 오늘 배운 내용 정리

인증 방식 사용 사례 핵심 코드
API Key 간단한 서비스 간 인증 request.headers.get('X-API-Key')
Bearer Token 웹/모바일 앱 인증 Authorization: Bearer <token>
OAuth 2.0 제3자 로그인 (구글, 카카오) Authorization Code Flow
JWT Stateless API, 마이크로서비스 jwt.encode(payload, secret)

핵심 코드 패턴

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 1. API 키 검증 데코레이터
def require_api_key(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        if request.headers.get('X-API-Key') != API_KEY:
            return jsonify({'error': 'Unauthorized'}), 401
        return f(*args, **kwargs)
    return decorated

# 2. JWT 생성
token = jwt.encode({
    'sub': user_id,
    'exp': datetime.utcnow() + timedelta(hours=1)
}, SECRET_KEY)

# 3. JWT 검증
payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])

# 4. 비밀번호 해싱
password_hash = generate_password_hash(password)
check_password_hash(password_hash, input_password)

🔗 관련 자료


📚 이전 학습

← Day 56: RESTful API 실전 REST 6 constraints, Richardson Maturity Model, 멱등성, CORS

📚 다음 학습

Day 58: 웹 크롤러 만들기 → 체계적인 웹 크롤러 아키텍처, 큐 기반 크롤링, 중복 방지


“늦었다고 생각할 때가 가장 빠를 때입니다.” API 인증, 처음엔 복잡해 보여도 하나씩 익히면 자연스러워집니다! 🔐

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.