[Python 100일 챌린지] Day 57 - API 인증
headers={'X-API-Key': 'secret-key'}→ 인증 성공! 없으면 401 Unauthorized! 😊API 키, JWT 토큰, OAuth… 내 API를 보호하는 방법! 아무나 접근 못하는 보안 API를 만듭니다!
(35-45분 완독 ⭐⭐⭐)
🎯 오늘의 학습 목표
📚 사전 지식
- Day 52: HTTP 메서드 기초 - HTTP 요청/응답 헤더
- Day 55: API 설계 기초 - 엔드포인트 구조와 응답 형식
- Day 56: RESTful API 실전 - HTTP 상태 코드와 에러 핸들링
🎯 학습 목표 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 등록
- GitHub → Settings → Developer settings → OAuth Apps
- New OAuth App 클릭
- 정보 입력:
- Application name:
My App - Homepage URL:
http://localhost:5000 - Authorization callback URL:
http://localhost:5000/callback
- Application name:
- 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" |
| 이메일 (커스텀) | "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 만들기
요구사항:
- API 키 인증을 사용하는 TODO API 작성
/api/todos(GET): 모든 TODO 조회/api/todos(POST): 새 TODO 추가- API 키가 없으면 401 에러 반환
- 사용자별로 다른 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 기반 사용자 등록과 로그인
요구사항:
/api/register(POST): 사용자 등록 (username, password)/api/login(POST): 로그인 → JWT 토큰 발급/api/me(GET): 현재 로그인한 사용자 정보 조회 (JWT 필요)- 비밀번호는 해싱하여 저장
- 토큰 만료 시간은 1시간
💡 힌트
werkzeug.security의generate_password_hash,check_password_hash사용jwt.encode와jwt.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)
🔗 관련 자료
- JWT 공식 사이트 - JWT 토큰 디버거
- OAuth 2.0 Simplified - OAuth 2.0 가이드
- Flask-JWT-Extended 공식 문서 - Flask JWT 라이브러리
- OWASP Authentication Cheat Sheet - 인증 보안 가이드
📚 이전 학습
← Day 56: RESTful API 실전 REST 6 constraints, Richardson Maturity Model, 멱등성, CORS
📚 다음 학습
Day 58: 웹 크롤러 만들기 → 체계적인 웹 크롤러 아키텍처, 큐 기반 크롤링, 중복 방지
“늦었다고 생각할 때가 가장 빠를 때입니다.” API 인증, 처음엔 복잡해 보여도 하나씩 익히면 자연스러워집니다! 🔐
