API ๋ณด์•ˆ

API ๋ณด์•ˆ

์ด์ „: 09_Web_Security_Headers.md | ๋‹ค์Œ: 11_Secrets_Management.md


API๋Š” ํ˜„๋Œ€ ์†Œํ”„ํŠธ์›จ์–ด ์‹œ์Šคํ…œ์˜ ํ•ต์‹ฌ์ž…๋‹ˆ๋‹ค. ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค๋ฅผ ์—ฐ๊ฒฐํ•˜๊ณ , ๋ชจ๋ฐ”์ผ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ตฌ๋™ํ•˜๋ฉฐ, ํƒ€์‚ฌ ํ†ตํ•ฉ์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค. API๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋กœ์ง๊ณผ ๋ฐ์ดํ„ฐ๋ฅผ ์ง์ ‘ ๋…ธ์ถœํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๊ณต๊ฒฉ์ž์˜ ์ฃผ์š” ํ‘œ์ ์ž…๋‹ˆ๋‹ค. ๋‹จ์ผ ์—”๋“œํฌ์ธํŠธ์˜ ์ž˜๋ชป๋œ ๊ตฌ์„ฑ์œผ๋กœ ์ˆ˜๋ฐฑ๋งŒ ๊ฐœ์˜ ๋ ˆ์ฝ”๋“œ๊ฐ€ ๋…ธ์ถœ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๋ ˆ์Šจ์€ API๋ฅผ ๊ตฌ์ถ•, ๋ฐฐํฌ ๋ฐ ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•œ ํ•„์ˆ˜ ๋ณด์•ˆ ๊ด€ํ–‰์„ ๋‹ค๋ฃน๋‹ˆ๋‹ค โ€” ์ธ์ฆ ๋ฐ ์†๋„ ์ œํ•œ๋ถ€ํ„ฐ ์ž…๋ ฅ ๊ฒ€์ฆ ๋ฐ CORS ๊ตฌ์„ฑ๊นŒ์ง€.

ํ•™์Šต ๋ชฉํ‘œ

  • API ํ‚ค, OAuth 2.0 ๋ฐ JWT๋ฅผ ์‚ฌ์šฉํ•œ ๊ฐ•๋ ฅํ•œ API ์ธ์ฆ ๊ตฌํ˜„
  • ๋‚จ์šฉ์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•œ ์†๋„ ์ œํ•œ ์ „๋žต ์„ค๊ณ„ ๋ฐ ๋ฐฐํฌ
  • ์ฃผ์ž… ๊ณต๊ฒฉ์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•œ ๋ชจ๋“  API ์ž…๋ ฅ ๊ฒ€์ฆ ๋ฐ ์ •์ œ
  • cross-origin ์ ‘๊ทผ์„ ์ œ์–ดํ•˜๊ธฐ ์œ„ํ•œ CORS ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌ์„ฑ
  • ์ผ๋ฐ˜์ ์ธ ๊ณต๊ฒฉ ํŒจํ„ด์œผ๋กœ๋ถ€ํ„ฐ GraphQL ์—”๋“œํฌ์ธํŠธ ๋ณดํ˜ธ
  • OpenAPI/Swagger ๋ช…์„ธ์—์„œ ๋ณด์•ˆ ์Šคํ‚ด ์ •์˜
  • ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์„ ์œ„ํ•œ API ๊ฒŒ์ดํŠธ์›จ์ด ๋ณด์•ˆ ํŒจํ„ด ์ ์šฉ

1. API ์œ„ํ˜‘ ํ™˜๊ฒฝ

1.1 OWASP API ๋ณด์•ˆ Top 10 (2023)

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                 OWASP API ๋ณด์•ˆ Top 10 (2023)                         โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                      โ”‚
โ”‚  API1  BOLA (Broken Object Level Authorization)                     โ”‚
โ”‚        ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž์˜ ๋ฆฌ์†Œ์Šค ์ ‘๊ทผ                                     โ”‚
โ”‚        GET /api/users/OTHER_USER_ID/orders                           โ”‚
โ”‚                                                                      โ”‚
โ”‚  API2  Broken Authentication                                         โ”‚
โ”‚        ์•ฝํ•œ ์ธ์ฆ ๋ฉ”์ปค๋‹ˆ์ฆ˜, ํ† ํฐ ์œ ์ถœ                                 โ”‚
โ”‚                                                                      โ”‚
โ”‚  API3  Broken Object Property Level Authorization                   โ”‚
โ”‚        ๋Œ€๋Ÿ‰ ํ• ๋‹น, ๋ฏผ๊ฐํ•œ ์†์„ฑ ๋…ธ์ถœ                                   โ”‚
โ”‚                                                                      โ”‚
โ”‚  API4  Unrestricted Resource Consumption                            โ”‚
โ”‚        ์†๋„ ์ œํ•œ ์—†์Œ, ํฐ ํŽ˜์ด๋กœ๋“œ, ๋น„์šฉ์ด ๋งŽ์ด ๋“œ๋Š” ์ฟผ๋ฆฌ            โ”‚
โ”‚                                                                      โ”‚
โ”‚  API5  Broken Function Level Authorization                          โ”‚
โ”‚        ์ผ๋ฐ˜ ์‚ฌ์šฉ์ž๋กœ์„œ ๊ด€๋ฆฌ ๊ธฐ๋Šฅ ์ ‘๊ทผ                                โ”‚
โ”‚        POST /api/admin/users/delete                                  โ”‚
โ”‚                                                                      โ”‚
โ”‚  API6  Unrestricted Access to Sensitive Business Flows              โ”‚
โ”‚        ๋น„์ฆˆ๋‹ˆ์Šค ๊ธฐ๋Šฅ์˜ ์ž๋™ํ™”๋œ ๋‚จ์šฉ                                 โ”‚
โ”‚                                                                      โ”‚
โ”‚  API7  Server-Side Request Forgery (SSRF)                           โ”‚
โ”‚        ๊ฒ€์ฆ ์—†์ด ์‚ฌ์šฉ์ž ์ž…๋ ฅ์—์„œ URL ๊ฐ€์ ธ์˜ค๊ธฐ                        โ”‚
โ”‚                                                                      โ”‚
โ”‚  API8  Security Misconfiguration                                    โ”‚
โ”‚        ํ—ค๋” ๋ˆ„๋ฝ, ์ž์„ธํ•œ ์˜ค๋ฅ˜, ๊ธฐ๋ณธ ์ž๊ฒฉ ์ฆ๋ช…                        โ”‚
โ”‚                                                                      โ”‚
โ”‚  API9  Improper Inventory Management                                โ”‚
โ”‚        ๊ด€๋ฆฌ๋˜์ง€ ์•Š๋Š” API ๋ฒ„์ „, ์„€๋„์šฐ API                            โ”‚
โ”‚                                                                      โ”‚
โ”‚  API10 Unsafe Consumption of APIs                                   โ”‚
โ”‚        ํƒ€์‚ฌ API ์‘๋‹ต์„ ๋งน๋ชฉ์ ์œผ๋กœ ์‹ ๋ขฐ                               โ”‚
โ”‚                                                                      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

1.2 API ๊ณต๊ฒฉ ํ‘œ๋ฉด

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    API ๊ณต๊ฒฉ ํ‘œ๋ฉด                                      โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                      โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”      โ”‚
โ”‚  โ”‚  Client  โ”‚โ”€โ”€โ”€โ–ถโ”‚ Network  โ”‚โ”€โ”€โ”€โ–ถโ”‚ API      โ”‚โ”€โ”€โ”€โ–ถโ”‚ Database โ”‚      โ”‚
โ”‚  โ”‚          โ”‚    โ”‚          โ”‚    โ”‚ Server   โ”‚    โ”‚          โ”‚      โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜      โ”‚
โ”‚                                                                      โ”‚
โ”‚  ๊ฐ ๊ณ„์ธต์˜ ๊ณต๊ฒฉ ๋ฒกํ„ฐ:                                                โ”‚
โ”‚                                                                      โ”‚
โ”‚  Client:     ๋ณ€์กฐ๋œ ์š”์ฒญ, ๋„๋‚œ๋‹นํ•œ ํ† ํฐ, ์žฌ์ƒ ๊ณต๊ฒฉ                   โ”‚
โ”‚  Network:    ์ค‘๊ฐ„์ž ๊ณต๊ฒฉ, ๋„์ฒญ, DNS ํ•˜์ด์žฌํ‚น                         โ”‚
โ”‚  API Server: ์ฃผ์ž…, ์ธ์ฆ ์†์ƒ, BOLA, ๋Œ€๋Ÿ‰ ํ• ๋‹น                       โ”‚
โ”‚  Database:   SQL ์ฃผ์ž…, ๋ฐ์ดํ„ฐ ์œ ์ถœ, ๋ฌด๋‹จ ์ ‘๊ทผ                        โ”‚
โ”‚                                                                      โ”‚
โ”‚  ํฌ๊ด„์ ์ธ ์šฐ๋ ค ์‚ฌํ•ญ:                                                 โ”‚
โ”‚  โ”œโ”€โ”€ ์ธ์ฆ         (๋‹น์‹ ์€ ๋ˆ„๊ตฌ์ธ๊ฐ€?)                                โ”‚
โ”‚  โ”œโ”€โ”€ ์ธ๊ฐ€         (๋ฌด์—‡์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋‚˜?)                          โ”‚
โ”‚  โ”œโ”€โ”€ ์ž…๋ ฅ ๊ฒ€์ฆ    (์ด ์š”์ฒญ์€ ์•ˆ์ „ํ•œ๊ฐ€?)                             โ”‚
โ”‚  โ”œโ”€โ”€ ์†๋„ ์ œํ•œ    (API๋ฅผ ๋‚จ์šฉํ•˜๊ณ  ์žˆ๋‚˜?)                            โ”‚
โ”‚  โ”œโ”€โ”€ ์•”ํ˜ธํ™”        (์ „์†ก ์ค‘ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณดํ˜ธ๋˜๋Š”๊ฐ€?)                    โ”‚
โ”‚  โ””โ”€โ”€ ๋กœ๊น…/๋ชจ๋‹ˆํ„ฐ๋ง (๊ณต๊ฒฉ์„ ๊ฐ์ง€ํ•  ์ˆ˜ ์žˆ๋‚˜?)                          โ”‚
โ”‚                                                                      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

2. API ์ธ์ฆ ํŒจํ„ด

2.1 API ํ‚ค

"""
API ํ‚ค ์ธ์ฆ โ€” ๋‹จ์ˆœํ•˜์ง€๋งŒ ์ œํ•œ์ .
์„œ๋ฒ„ ๊ฐ„ ํ†ต์‹  ๋ฐ ์‚ฌ์šฉ ์ถ”์ ์ด ์žˆ๋Š” ๊ณต๊ฐœ API์— ์ ํ•ฉ.
"""
import secrets
import hashlib
from flask import Flask, request, jsonify, abort
from functools import wraps
from datetime import datetime

app = Flask(__name__)

# โ”€โ”€ API ํ‚ค ์ƒ์„ฑ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def generate_api_key() -> tuple[str, str]:
    """API ํ‚ค์™€ ์ €์žฅ์šฉ ํ•ด์‹œ๋ฅผ ์ƒ์„ฑ."""
    # ์•”ํ˜ธํ•™์ ์œผ๋กœ ์•ˆ์ „ํ•œ ํ‚ค ์ƒ์„ฑ
    # ์ ‘๋‘์‚ฌ๋Š” ํ‚ค ์œ ํ˜•๊ณผ ๋ฒ„์ „์„ ์‹๋ณ„ํ•˜๋Š” ๋ฐ ๋„์›€
    raw_key = f"sk_live_{secrets.token_urlsafe(32)}"

    # ์›์‹œ ํ‚ค๋Š” ์ ˆ๋Œ€ ์ €์žฅํ•˜์ง€ ๋ง๊ณ  ํ•ด์‹œ๋งŒ ์ €์žฅ
    key_hash = hashlib.sha256(raw_key.encode()).hexdigest()

    return raw_key, key_hash

# raw_key: sk_live_Abc123...  (ํด๋ผ์ด์–ธํŠธ์— ํ•œ ๋ฒˆ ์ „์†ก, ์ €์žฅ ์•ˆ ํ•จ)
# key_hash: e3b0c4...         (๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅ)


# โ”€โ”€ ์‹œ๋ฎฌ๋ ˆ์ด์…˜๋œ ํ‚ค ์ €์žฅ์†Œ (ํ”„๋กœ๋•์…˜์—์„œ๋Š” ์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์‚ฌ์šฉ) โ”€โ”€โ”€โ”€
API_KEYS = {
    # ํ•ด์‹œ -> ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ
    hashlib.sha256(b"sk_live_test_key_12345").hexdigest(): {
        "client_id": "client_001",
        "name": "Test Client",
        "rate_limit": 100,       # ๋ถ„๋‹น ์š”์ฒญ ์ˆ˜
        "scopes": ["read", "write"],
        "created_at": "2025-01-01",
        "last_used": None,
    }
}


def require_api_key(f):
    """์œ ํšจํ•œ API ํ‚ค๋ฅผ ์š”๊ตฌํ•˜๋Š” ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ."""
    @wraps(f)
    def decorated(*args, **kwargs):
        # API ํ‚ค๋ฅผ ์—ฌ๋Ÿฌ ์œ„์น˜์—์„œ ํ™•์ธ
        api_key = (
            request.headers.get('X-API-Key') or
            request.headers.get('Authorization', '').replace('Bearer ', '') or
            request.args.get('api_key')  # ๋œ ์•ˆ์ „, ๊ฐ€๋Šฅํ•˜๋ฉด ํ”ผํ•˜๊ธฐ
        )

        if not api_key:
            return jsonify({
                "error": "missing_api_key",
                "message": "API ํ‚ค๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. "
                           "X-API-Key ํ—ค๋”์— ์ „๋‹ฌํ•˜์„ธ์š”."
            }), 401

        # ์ œ๊ณต๋œ ํ‚ค๋ฅผ ํ•ด์‹œํ•˜๊ณ  ์กฐํšŒ
        key_hash = hashlib.sha256(api_key.encode()).hexdigest()
        key_data = API_KEYS.get(key_hash)

        if not key_data:
            return jsonify({
                "error": "invalid_api_key",
                "message": "์ œ๊ณต๋œ API ํ‚ค๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."
            }), 401

        # ๋งˆ์ง€๋ง‰ ์‚ฌ์šฉ ํƒ€์ž„์Šคํƒฌํ”„ ์—…๋ฐ์ดํŠธ
        key_data["last_used"] = datetime.utcnow().isoformat()

        # ์š”์ฒญ ์ปจํ…์ŠคํŠธ์— ํ‚ค ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ €์žฅ
        request.api_client = key_data
        return f(*args, **kwargs)

    return decorated


@app.route('/api/data')
@require_api_key
def get_data():
    """API ํ‚ค๊ฐ€ ํ•„์š”ํ•œ ๋ณดํ˜ธ๋œ ์—”๋“œํฌ์ธํŠธ."""
    client = request.api_client
    return jsonify({
        "client": client["name"],
        "data": [1, 2, 3]
    })


# โ”€โ”€ API ํ‚ค ๋ณด์•ˆ ๋ชจ๋ฒ” ์‚ฌ๋ก€ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
"""
1. ์ „์†ก:
   - ํ•ญ์ƒ HTTPS ์‚ฌ์šฉ
   - ์ฟผ๋ฆฌ ๋งค๊ฐœ๋ณ€์ˆ˜๋ณด๋‹ค ํ—ค๋” ์„ ํ˜ธ (์ฟผ๋ฆฌ ๋งค๊ฐœ๋ณ€์ˆ˜๋Š” ๋กœ๊ทธ์— ๋‚˜ํƒ€๋‚จ)
   - X-API-Key ํ—ค๋” ๋˜๋Š” Authorization: Bearer <key> ์‚ฌ์šฉ

2. ์ €์žฅ:
   - ์ €์žฅํ•˜๊ธฐ ์ „์— ํ‚ค ํ•ด์‹œ (์ตœ์†Œ SHA-256)
   - ์ „์ฒด API ํ‚ค๋ฅผ ๋กœ๊ทธ์— ๋‚จ๊ธฐ์ง€ ๋ง ๊ฒƒ
   - UI์—๋Š” ๋งˆ์ง€๋ง‰ 4์ž๋งŒ ํ‘œ์‹œ: sk_live_****5678

3. ์ˆœํ™˜:
   - ํด๋ผ์ด์–ธํŠธ๋‹น ์—ฌ๋Ÿฌ ํ™œ์„ฑ ํ‚ค ์ง€์›
   - ๋‹ค์šดํƒ€์ž„ ์—†์ด ํ‚ค ์ˆœํ™˜ ํ—ˆ์šฉ
   - ํ‚ค์— ๋งŒ๋ฃŒ์ผ ์„ค์ •

4. ๋ฒ”์œ„ ์ง€์ •:
   - ๊ฐ ํ‚ค์— ๋ฒ”์œ„/๊ถŒํ•œ ํ• ๋‹น
   - ์ฝ๊ธฐ ๋Œ€ ์“ฐ๊ธฐ ์ž‘์—…์— ๋ณ„๋„ ํ‚ค ์‚ฌ์šฉ
   - ํ…Œ์ŠคํŠธ ๋Œ€ ํ”„๋กœ๋•์…˜์— ๋ณ„๋„ ํ‚ค ์‚ฌ์šฉ

5. ์ œํ•œ ์‚ฌํ•ญ:
   - API ํ‚ค๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์•„๋‹Œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์‹๋ณ„
   - ๋‚ด์žฅ ๋งŒ๋ฃŒ๊ฐ€ ๋ถ€์กฑ (ํ† ํฐ๊ณผ ๋‹ฌ๋ฆฌ)
   - ์š”์ฒญ๋ณ„๋กœ ์‰ฝ๊ฒŒ ๋ฒ”์œ„๋ฅผ ์ง€์ •ํ•  ์ˆ˜ ์—†์Œ
   - ์‚ฌ์šฉ์ž ์ปจํ…์ŠคํŠธ ์ธ๊ฐ€์—๋Š” OAuth 2.0 ์‚ฌ์šฉ
"""

2.2 OAuth 2.0

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    OAuth 2.0 Authorization Code ํ๋ฆ„                 โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                      โ”‚
โ”‚  1. ์‚ฌ์šฉ์ž๊ฐ€ "์ œ๊ณต์ž๋กœ ๋กœ๊ทธ์ธ" ํด๋ฆญ                                  โ”‚
โ”‚     Client โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ Auth Server                   โ”‚
โ”‚     GET /authorize?response_type=code                                โ”‚
โ”‚         &client_id=CLIENT_ID                                         โ”‚
โ”‚         &redirect_uri=CALLBACK_URL                                   โ”‚
โ”‚         &scope=read+write                                            โ”‚
โ”‚         &state=RANDOM_STATE                                          โ”‚
โ”‚                                                                      โ”‚
โ”‚  2. ์‚ฌ์šฉ์ž ์ธ์ฆ ๋ฐ ๋ฒ”์œ„ ์Šน์ธ                                         โ”‚
โ”‚     Auth Server โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ Client Callback               โ”‚
โ”‚     GET /callback?code=AUTH_CODE&state=RANDOM_STATE                  โ”‚
โ”‚                                                                      โ”‚
โ”‚  3. ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ฝ”๋“œ๋ฅผ ํ† ํฐ์œผ๋กœ ๊ตํ™˜                                โ”‚
โ”‚     Client โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ Auth Server                   โ”‚
โ”‚     POST /token                                                      โ”‚
โ”‚         grant_type=authorization_code                                โ”‚
โ”‚         &code=AUTH_CODE                                              โ”‚
โ”‚         &client_id=CLIENT_ID                                         โ”‚
โ”‚         &client_secret=CLIENT_SECRET                                 โ”‚
โ”‚         &redirect_uri=CALLBACK_URL                                   โ”‚
โ”‚                                                                      โ”‚
โ”‚  4. ์ธ์ฆ ์„œ๋ฒ„๊ฐ€ ํ† ํฐ ๋ฐ˜ํ™˜                                            โ”‚
โ”‚     Auth Server โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ Client                        โ”‚
โ”‚     { "access_token": "...",                                         โ”‚
โ”‚       "refresh_token": "...",                                        โ”‚
โ”‚       "token_type": "Bearer",                                        โ”‚
โ”‚       "expires_in": 3600 }                                           โ”‚
โ”‚                                                                      โ”‚
โ”‚  5. ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์•ก์„ธ์Šค ํ† ํฐ ์‚ฌ์šฉ                                    โ”‚
โ”‚     Client โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ Resource Server               โ”‚
โ”‚     Authorization: Bearer ACCESS_TOKEN                               โ”‚
โ”‚                                                                      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
"""
Flask๋ฅผ ์‚ฌ์šฉํ•œ OAuth 2.0 ๊ตฌํ˜„ (์„œ๋ฒ„ ์ธก).
"""
import requests
import secrets
from flask import Flask, redirect, request, session, jsonify
from urllib.parse import urlencode

app = Flask(__name__)
app.secret_key = secrets.token_hex(32)

# OAuth 2.0 ๊ตฌ์„ฑ (์˜ˆ: GitHub)
OAUTH_CONFIG = {
    "client_id": "your_client_id",
    "client_secret": "your_client_secret",
    "authorize_url": "https://github.com/login/oauth/authorize",
    "token_url": "https://github.com/login/oauth/access_token",
    "api_url": "https://api.github.com/user",
    "redirect_uri": "http://localhost:5000/callback",
    "scope": "read:user user:email",
}


@app.route('/login')
def login():
    """OAuth 2.0 Authorization Code ํ๋ฆ„ ์‹œ์ž‘."""
    # CSRF๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•œ state ๋งค๊ฐœ๋ณ€์ˆ˜ ์ƒ์„ฑ
    state = secrets.token_urlsafe(32)
    session['oauth_state'] = state

    # ์ธ๊ฐ€ URL ๊ตฌ์ถ•
    params = {
        "client_id": OAUTH_CONFIG["client_id"],
        "redirect_uri": OAUTH_CONFIG["redirect_uri"],
        "scope": OAUTH_CONFIG["scope"],
        "state": state,
        "response_type": "code",
    }

    auth_url = f"{OAUTH_CONFIG['authorize_url']}?{urlencode(params)}"
    return redirect(auth_url)


@app.route('/callback')
def callback():
    """OAuth 2.0 ์ฝœ๋ฐฑ ์ฒ˜๋ฆฌ."""
    # โ”€โ”€ state ๋งค๊ฐœ๋ณ€์ˆ˜ ํ™•์ธ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    state = request.args.get('state')
    if state != session.pop('oauth_state', None):
        return jsonify({"error": "์œ ํšจํ•˜์ง€ ์•Š์€ state ๋งค๊ฐœ๋ณ€์ˆ˜"}), 400

    # โ”€โ”€ ์˜ค๋ฅ˜ ์‘๋‹ต ํ™•์ธ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    error = request.args.get('error')
    if error:
        return jsonify({
            "error": error,
            "description": request.args.get('error_description', '')
        }), 400

    # โ”€โ”€ ์ธ๊ฐ€ ์ฝ”๋“œ๋ฅผ ํ† ํฐ์œผ๋กœ ๊ตํ™˜ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    code = request.args.get('code')
    token_response = requests.post(
        OAUTH_CONFIG["token_url"],
        data={
            "client_id": OAUTH_CONFIG["client_id"],
            "client_secret": OAUTH_CONFIG["client_secret"],
            "code": code,
            "redirect_uri": OAUTH_CONFIG["redirect_uri"],
            "grant_type": "authorization_code",
        },
        headers={"Accept": "application/json"},
        timeout=10,
    )
    token_data = token_response.json()

    if "error" in token_data:
        return jsonify(token_data), 400

    access_token = token_data["access_token"]

    # โ”€โ”€ ์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    user_response = requests.get(
        OAUTH_CONFIG["api_url"],
        headers={
            "Authorization": f"Bearer {access_token}",
            "Accept": "application/json",
        },
        timeout=10,
    )
    user_data = user_response.json()

    # ์„ธ์…˜์— ์ €์žฅ (๋˜๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์‚ฌ์šฉ์ž ์ƒ์„ฑ/์—…๋ฐ์ดํŠธ)
    session['user'] = {
        "id": user_data["id"],
        "name": user_data.get("name", user_data["login"]),
        "email": user_data.get("email"),
    }

    return redirect('/dashboard')


# โ”€โ”€ OAuth 2.0 ๋ณด์•ˆ ์ฒดํฌ๋ฆฌ์ŠคํŠธ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
"""
1. ํ•ญ์ƒ state ๋งค๊ฐœ๋ณ€์ˆ˜ ์‚ฌ์šฉ (CSRF ๋ฐฉ์ง€)
2. redirect_uri๋ฅผ ์ •ํ™•ํžˆ ๊ฒ€์ฆ (์˜คํ”ˆ ๋ฆฌ๋””๋ ‰ํŠธ ๋ฐฉ์ง€)
3. ์„œ๋ฒ„ ์•ฑ์— Authorization Code ํ๋ฆ„ ์‚ฌ์šฉ (Implicit ์•„๋‹˜)
4. ํ† ํฐ์„ ์•ˆ์ „ํ•˜๊ฒŒ ์ €์žฅ (์•”ํ˜ธํ™”, ์„œ๋ฒ„ ์ธก)
5. ๊ณต๊ฐœ ํด๋ผ์ด์–ธํŠธ(SPA, ๋ชจ๋ฐ”์ผ ์•ฑ)์— PKCE ์‚ฌ์šฉ
6. ๋ชจ๋“  ์š”์ฒญ์—์„œ ํ† ํฐ ๋ฒ”์œ„ ๊ฒ€์ฆ
7. ๋‹จ๊ธฐ ์•ก์„ธ์Šค ํ† ํฐ + ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ์‚ฌ์šฉ
8. ๋กœ๊ทธ์•„์›ƒ ์‹œ ํ† ํฐ ํ๊ธฐ
"""

2.3 JWT (JSON Web Tokens)

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    JWT ๊ตฌ์กฐ                                          โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                      โ”‚
โ”‚  eyJhbGci... . eyJzdWIi... . SflKxwRJ...                           โ”‚
โ”‚  โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€     โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€     โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€                             โ”‚
โ”‚   Header        Payload       Signature                              โ”‚
โ”‚                                                                      โ”‚
โ”‚  Header (base64url):                                                 โ”‚
โ”‚  {                                                                   โ”‚
โ”‚    "alg": "RS256",                                                   โ”‚
โ”‚    "typ": "JWT",                                                     โ”‚
โ”‚    "kid": "key-id-123"                                               โ”‚
โ”‚  }                                                                   โ”‚
โ”‚                                                                      โ”‚
โ”‚  Payload (base64url):                                                โ”‚
โ”‚  {                                                                   โ”‚
โ”‚    "sub": "user_123",        // Subject (์‚ฌ์šฉ์ž ID)                  โ”‚
โ”‚    "iss": "api.example.com", // Issuer                               โ”‚
โ”‚    "aud": "app.example.com", // Audience                             โ”‚
โ”‚    "exp": 1700000000,        // ๋งŒ๋ฃŒ ์‹œ๊ฐ„                            โ”‚
โ”‚    "iat": 1699996400,        // ๋ฐœ๊ธ‰ ์‹œ๊ฐ„                            โ”‚
โ”‚    "nbf": 1699996400,        // Not before                           โ”‚
โ”‚    "jti": "unique-token-id", // JWT ID (ํ๊ธฐ์šฉ)                      โ”‚
โ”‚    "scope": "read write",    // ์‚ฌ์šฉ์ž ์ •์˜ ํด๋ ˆ์ž„                   โ”‚
โ”‚    "role": "admin"                                                   โ”‚
โ”‚  }                                                                   โ”‚
โ”‚                                                                      โ”‚
โ”‚  Signature:                                                          โ”‚
โ”‚  RSASHA256(base64url(header) + "." + base64url(payload), key)       โ”‚
โ”‚                                                                      โ”‚
โ”‚  ์ค‘์š”: Payload๋Š” ์•”ํ˜ธํ™”๋˜์ง€ ์•Š์Œ โ€” base64url ์ธ์ฝ”๋”ฉ๋งŒ ๋จ            โ”‚
โ”‚  ๋ˆ„๊ตฌ๋‚˜ ์ฝ์„ ์ˆ˜ ์žˆ์Œ. JWT์— ๋น„๋ฐ€์„ ์ €์žฅํ•˜์ง€ ๋ง ๊ฒƒ.                   โ”‚
โ”‚                                                                      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
"""
PyJWT๋ฅผ ์‚ฌ์šฉํ•œ JWT ์ธ์ฆ โ€” ์•ˆ์ „ํ•œ ๊ตฌํ˜„.
"""
import jwt
import time
import uuid
from datetime import datetime, timedelta, timezone
from flask import Flask, request, jsonify
from functools import wraps

app = Flask(__name__)

# โ”€โ”€ ํ‚ค ๊ตฌ์„ฑ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# ํ”„๋กœ๋•์…˜์—๋Š” RS256 (๋น„๋Œ€์นญ) ์‚ฌ์šฉ
# ๊ฐœ์ธ ํ‚ค๋Š” ํ† ํฐ์— ์„œ๋ช…; ๊ณต๊ฐœ ํ‚ค๋Š” ๊ฒ€์ฆ
# ์ด๋ฅผ ํ†ตํ•ด ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค๊ฐ€ ๊ฐœ์ธ ํ‚ค ์—†์ด ๊ฒ€์ฆ ๊ฐ€๋Šฅ

# ์ด ์˜ˆ์ œ์—์„œ๋Š” ๋‹จ์ˆœํ™”๋ฅผ ์œ„ํ•ด HS256 (๋Œ€์นญ) ์‚ฌ์šฉ
JWT_SECRET = "your-256-bit-secret-change-this"  # ํ”„๋กœ๋•์…˜์—์„œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์‚ฌ์šฉ
JWT_ALGORITHM = "HS256"
JWT_ACCESS_TOKEN_EXPIRES = timedelta(minutes=15)
JWT_REFRESH_TOKEN_EXPIRES = timedelta(days=30)


# โ”€โ”€ ํ† ํฐ ๋ธ”๋ž™๋ฆฌ์ŠคํŠธ (ํ”„๋กœ๋•์…˜์—์„œ Redis ์‚ฌ์šฉ) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
revoked_tokens = set()


def create_access_token(user_id: str, role: str = "user",
                        scopes: list[str] = None) -> str:
    """๋‹จ๊ธฐ ์•ก์„ธ์Šค ํ† ํฐ ์ƒ์„ฑ."""
    now = datetime.now(timezone.utc)
    payload = {
        "sub": user_id,
        "iss": "api.example.com",
        "aud": "app.example.com",
        "iat": now,
        "exp": now + JWT_ACCESS_TOKEN_EXPIRES,
        "nbf": now,
        "jti": str(uuid.uuid4()),        # ํ๊ธฐ๋ฅผ ์œ„ํ•œ ๊ณ ์œ  ID
        "type": "access",
        "role": role,
        "scopes": scopes or ["read"],
    }
    return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)


def create_refresh_token(user_id: str) -> str:
    """์žฅ๊ธฐ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ์ƒ์„ฑ."""
    now = datetime.now(timezone.utc)
    payload = {
        "sub": user_id,
        "iss": "api.example.com",
        "iat": now,
        "exp": now + JWT_REFRESH_TOKEN_EXPIRES,
        "jti": str(uuid.uuid4()),
        "type": "refresh",
    }
    return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)


def decode_token(token: str) -> dict:
    """JWT ํ† ํฐ ๋””์ฝ”๋“œ ๋ฐ ๊ฒ€์ฆ."""
    try:
        payload = jwt.decode(
            token,
            JWT_SECRET,
            algorithms=[JWT_ALGORITHM],
            issuer="api.example.com",
            audience="app.example.com",
            options={
                "require": ["exp", "iat", "sub", "iss", "aud", "jti"],
                "verify_exp": True,
                "verify_iss": True,
                "verify_aud": True,
                "verify_nbf": True,
            }
        )

        # ํ† ํฐ์ด ํ๊ธฐ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ
        if payload["jti"] in revoked_tokens:
            raise jwt.InvalidTokenError("ํ† ํฐ์ด ํ๊ธฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค")

        return payload

    except jwt.ExpiredSignatureError:
        raise ValueError("ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค")
    except jwt.InvalidAudienceError:
        raise ValueError("์œ ํšจํ•˜์ง€ ์•Š์€ ํ† ํฐ ๋Œ€์ƒ")
    except jwt.InvalidIssuerError:
        raise ValueError("์œ ํšจํ•˜์ง€ ์•Š์€ ํ† ํฐ ๋ฐœ๊ธ‰์ž")
    except jwt.InvalidTokenError as e:
        raise ValueError(f"์œ ํšจํ•˜์ง€ ์•Š์€ ํ† ํฐ: {e}")


def require_auth(scopes: list[str] = None):
    """์„ ํƒ์  ๋ฒ”์œ„ ํ™•์ธ๊ณผ ํ•จ๊ป˜ JWT ์ธ์ฆ์„ ์š”๊ตฌํ•˜๋Š” ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ."""
    def decorator(f):
        @wraps(f)
        def decorated(*args, **kwargs):
            # Authorization ํ—ค๋”์—์„œ ํ† ํฐ ์ถ”์ถœ
            auth_header = request.headers.get('Authorization', '')
            if not auth_header.startswith('Bearer '):
                return jsonify({
                    "error": "missing_token",
                    "message": "Bearer ํ† ํฐ์ด ์žˆ๋Š” Authorization ํ—ค๋” ํ•„์š”"
                }), 401

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

            try:
                payload = decode_token(token)
            except ValueError as e:
                return jsonify({
                    "error": "invalid_token",
                    "message": str(e)
                }), 401

            # ํ† ํฐ ์œ ํ˜• ํ™•์ธ
            if payload.get("type") != "access":
                return jsonify({
                    "error": "wrong_token_type",
                    "message": "์•ก์„ธ์Šค ํ† ํฐ ํ•„์š”"
                }), 401

            # ๋ฒ”์œ„ ํ™•์ธ
            if scopes:
                token_scopes = set(payload.get("scopes", []))
                required_scopes = set(scopes)
                if not required_scopes.issubset(token_scopes):
                    missing = required_scopes - token_scopes
                    return jsonify({
                        "error": "insufficient_scope",
                        "message": f"๋ˆ„๋ฝ๋œ ๋ฒ”์œ„: {', '.join(missing)}"
                    }), 403

            request.user = payload
            return f(*args, **kwargs)
        return decorated
    return decorator


# โ”€โ”€ ๋ผ์šฐํŠธ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@app.route('/api/login', methods=['POST'])
def login():
    """์ธ์ฆํ•˜๊ณ  JWT ํ† ํฐ ๋ฐ˜ํ™˜."""
    data = request.get_json()
    username = data.get('username')
    password = data.get('password')

    # ์ž๊ฒฉ ์ฆ๋ช… ๊ฒ€์ฆ (bcrypt/argon2 ํ•ด์‹œ ๋น„๊ต ์‚ฌ์šฉ)
    # ๋‹จ์ˆœํ™”๋จ โ€” ์ž์„ธํ•œ ๋‚ด์šฉ์€ ์ธ์ฆ ๋ ˆ์Šจ ์ฐธ์กฐ
    user = authenticate_user(username, password)
    if not user:
        # ์‚ฌ์šฉ์ž ์—ด๊ฑฐ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ ์ผ๊ด€๋œ ํƒ€์ด๋ฐ ์‚ฌ์šฉ
        return jsonify({
            "error": "invalid_credentials",
            "message": "์œ ํšจํ•˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž ์ด๋ฆ„ ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ"
        }), 401

    # ํ† ํฐ ์Œ ์ƒ์„ฑ
    access_token = create_access_token(
        user_id=user["id"],
        role=user["role"],
        scopes=user["scopes"]
    )
    refresh_token = create_refresh_token(user_id=user["id"])

    return jsonify({
        "access_token": access_token,
        "refresh_token": refresh_token,
        "token_type": "Bearer",
        "expires_in": int(JWT_ACCESS_TOKEN_EXPIRES.total_seconds()),
    })


@app.route('/api/refresh', methods=['POST'])
def refresh():
    """๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์„ ์ƒˆ ์•ก์„ธ์Šค ํ† ํฐ์œผ๋กœ ๊ตํ™˜."""
    data = request.get_json()
    refresh_token = data.get('refresh_token')

    if not refresh_token:
        return jsonify({"error": "missing_refresh_token"}), 400

    try:
        # ๋Œ€์ƒ ํ™•์ธ ์—†์ด ๋””์ฝ”๋“œ (๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์€ ๋‹ค๋ฅผ ์ˆ˜ ์žˆ์Œ)
        payload = jwt.decode(
            refresh_token,
            JWT_SECRET,
            algorithms=[JWT_ALGORITHM],
            issuer="api.example.com",
            options={"require": ["exp", "sub", "jti", "iss"]}
        )

        if payload.get("type") != "refresh":
            raise ValueError("๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์ด ์•„๋‹˜")

        if payload["jti"] in revoked_tokens:
            raise ValueError("๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์ด ํ๊ธฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค")

    except (jwt.InvalidTokenError, ValueError) as e:
        return jsonify({"error": "invalid_refresh_token",
                        "message": str(e)}), 401

    # ์˜ค๋ž˜๋œ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ํ๊ธฐ (์ˆœํ™˜)
    revoked_tokens.add(payload["jti"])

    # ์ƒˆ ํ† ํฐ ์Œ ๋ฐœ๊ธ‰
    user_id = payload["sub"]
    # ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ํ˜„์žฌ ์‚ฌ์šฉ์ž ์—ญํ• /๋ฒ”์œ„ ์กฐํšŒ
    user = get_user_by_id(user_id)

    new_access = create_access_token(
        user_id=user_id,
        role=user["role"],
        scopes=user["scopes"]
    )
    new_refresh = create_refresh_token(user_id=user_id)

    return jsonify({
        "access_token": new_access,
        "refresh_token": new_refresh,
        "token_type": "Bearer",
        "expires_in": int(JWT_ACCESS_TOKEN_EXPIRES.total_seconds()),
    })


@app.route('/api/logout', methods=['POST'])
@require_auth()
def logout():
    """ํ˜„์žฌ ์•ก์„ธ์Šค ํ† ํฐ ํ๊ธฐ."""
    revoked_tokens.add(request.user["jti"])
    return jsonify({"message": "์„ฑ๊ณต์ ์œผ๋กœ ๋กœ๊ทธ์•„์›ƒํ–ˆ์Šต๋‹ˆ๋‹ค"}), 200


@app.route('/api/protected')
@require_auth(scopes=["read"])
def protected_resource():
    """'read' ๋ฒ”์œ„๊ฐ€ ํ•„์š”ํ•œ ๋ณดํ˜ธ๋œ ์—”๋“œํฌ์ธํŠธ."""
    return jsonify({
        "user_id": request.user["sub"],
        "message": "์ด ๋ณดํ˜ธ๋œ ๋ฆฌ์†Œ์Šค์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค"
    })


# ์™„์ „์„ฑ์„ ์œ„ํ•œ ํ”Œ๋ ˆ์ด์Šคํ™€๋” ํ•จ์ˆ˜
def authenticate_user(username, password):
    """ํ”Œ๋ ˆ์ด์Šคํ™€๋” โ€” ์ ์ ˆํ•œ ๋น„๋ฐ€๋ฒˆํ˜ธ ํ•ด์‹ฑ์œผ๋กœ ๊ตฌํ˜„."""
    return None

def get_user_by_id(user_id):
    """ํ”Œ๋ ˆ์ด์Šคํ™€๋” โ€” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์กฐํšŒ๋กœ ๊ตฌํ˜„."""
    return {"id": user_id, "role": "user", "scopes": ["read"]}

2.4 JWT ๋ณด์•ˆ ๋ชจ๋ฒ” ์‚ฌ๋ก€

"""
JWT ๋ณด์•ˆ ๋ชจ๋ฒ” ์‚ฌ๋ก€ ๋ฐ ์ผ๋ฐ˜์ ์ธ ํ•จ์ •.
"""

# โ”€โ”€ ํ•จ์ • 1: 'none' ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์‚ฌ์šฉ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# ๊ณต๊ฒฉ: ํ—ค๋”๋ฅผ {"alg": "none"}์œผ๋กœ ๋ณ€๊ฒฝํ•˜๊ณ  ์„œ๋ช… ์ œ๊ฑฐ
# ๋ฐฉ์–ด: ํ•ญ์ƒ ํ—ˆ์šฉ๋œ ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ๋ช…์‹œ์ ์œผ๋กœ ์ง€์ •
payload = jwt.decode(
    token, key,
    algorithms=["RS256"],  # "none"์„ ์ ˆ๋Œ€ ํฌํ•จํ•˜๊ฑฐ๋‚˜ ๋ชจ๋‘ ํ—ˆ์šฉํ•˜์ง€ ๋ง ๊ฒƒ
)

# โ”€โ”€ ํ•จ์ • 2: HS256๊ณผ RS256 ํ˜ผ๋™ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# ๊ณต๊ฒฉ: ์„œ๋ฒ„๊ฐ€ RS256์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ, ๊ณต๊ฒฉ์ž๊ฐ€:
#   1. ๊ณต๊ฐœ ํ‚ค ๊ฐ€์ ธ์˜ค๊ธฐ (๊ณต๊ฐœ์ž„)
#   2. ๊ณต๊ฐœ ํ‚ค๋ฅผ ๋น„๋ฐ€๋กœ ์‚ฌ์šฉํ•˜์—ฌ HS256์œผ๋กœ ์ƒˆ ํ† ํฐ ์„œ๋ช…
#   3. ์„œ๋ฒ„๊ฐ€ ๊ณต๊ฐœ ํ‚ค๋ฅผ HMAC ๋น„๋ฐ€๋กœ ์ทจ๊ธ‰
# ๋ฐฉ์–ด: ํ—ค๋”์˜ ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ์—„๊ฒฉํ•˜๊ฒŒ ๊ฒ€์ฆ
payload = jwt.decode(
    token, rsa_public_key,
    algorithms=["RS256"],  # ์˜ˆ์ƒ๋œ ์•Œ๊ณ ๋ฆฌ์ฆ˜๋งŒ ํ—ˆ์šฉ
)

# โ”€โ”€ ํ•จ์ • 3: ๋งŒ๋ฃŒ ๋ˆ„๋ฝ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# exp ํด๋ ˆ์ž„์ด ์—†๋Š” ํ† ํฐ์€ ์˜์›ํžˆ ์œ ํšจ
# ๋ฐฉ์–ด: ํ•ญ์ƒ ์งง์€ ๋งŒ๋ฃŒ ์„ค์ •
payload = {
    "sub": "user_123",
    "exp": datetime.now(timezone.utc) + timedelta(minutes=15),
}

# โ”€โ”€ ํ•จ์ • 4: ํŽ˜์ด๋กœ๋“œ์— ๋ฏผ๊ฐํ•œ ๋ฐ์ดํ„ฐ ์ €์žฅ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# JWT ํŽ˜์ด๋กœ๋“œ๋Š” base64url ์ธ์ฝ”๋”ฉ๋˜๋ฉฐ ์•”ํ˜ธํ™”๋˜์ง€ ์•Š์Œ
# ํ‚ค ์—†์ด๋„ ๋ˆ„๊ตฌ๋‚˜ ๋””์ฝ”๋“œ ๊ฐ€๋Šฅ
import base64
header, payload_b64, signature = token.split('.')
decoded = base64.urlsafe_b64decode(payload_b64 + '==')
# ์ „์ฒด ํŽ˜์ด๋กœ๋“œ๋ฅผ ์ด์ œ ์ฝ์„ ์ˆ˜ ์žˆ์Œ!

# ์ ˆ๋Œ€ ํฌํ•จํ•˜์ง€ ๋ง ๊ฒƒ: ๋น„๋ฐ€๋ฒˆํ˜ธ, SSN, ์‹ ์šฉ์นด๋“œ, PII๋ฅผ JWT์—
# ํฌํ•จํ•  ๊ฒƒ๋งŒ: ์‚ฌ์šฉ์ž ID, ์—ญํ• , ๋ฒ”์œ„, ๋งŒ๋ฃŒ

# โ”€โ”€ ํ•จ์ • 5: ํ† ํฐ ํ๊ธฐ ๋ฉ”์ปค๋‹ˆ์ฆ˜ ์—†์Œ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# JWT๋Š” ์ž์ฒด ํฌํ•จ โ€” ์„œ๋ฒ„๊ฐ€ ๋ฌดํšจํ™”ํ•  ์ˆ˜ ์—†์Œ
# ํ•ด๊ฒฐ์ฑ…:
# 1. ๋‹จ๊ธฐ ์•ก์„ธ์Šค ํ† ํฐ (15๋ถ„) + ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ์ˆœํ™˜
# 2. ํ† ํฐ ๋ธ”๋ž™๋ฆฌ์ŠคํŠธ (TTL = ํ† ํฐ exp๊ฐ€ ์žˆ๋Š” Redis)
# 3. ํ† ํฐ ๋ฒ„์ „ ๊ด€๋ฆฌ (DB์— ํ† ํฐ ๋ฒ„์ „ ์ €์žฅ, ๊ฐ ์š”์ฒญ์—์„œ ํ™•์ธ)
# 4. ์„œ๋ช… ํ‚ค ๋ณ€๊ฒฝ (๋ชจ๋“  ํ† ํฐ ๋ฌดํšจํ™” โ€” ๊ทน๋‹จ์  ์˜ต์…˜)

# โ”€โ”€ RS256 ์„ค์ • (ํ”„๋กœ๋•์…˜ ๊ถŒ์žฅ) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
"""
# RSA ํ‚ค ์Œ ์ƒ์„ฑ
openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -pubout -out public.pem

# ๊ฐœ์ธ ํ‚ค: ์ธ์ฆ ์„œ๋ฒ„๊ฐ€ ํ† ํฐ์— ์„œ๋ช…ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ
# ๊ณต๊ฐœ ํ‚ค: ๋ชจ๋“  ์„œ๋น„์Šค๊ฐ€ ํ† ํฐ์„ ๊ฒ€์ฆํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ
# ๊ณต๊ฐœ ํ‚ค๋ฅผ ์ž์œ ๋กญ๊ฒŒ ๊ณต์œ ; ๊ฐœ์ธ ํ‚ค ๋ณดํ˜ธ
"""

from cryptography.hazmat.primitives import serialization

# ํ‚ค ๋กœ๋“œ
with open('private.pem', 'rb') as f:
    private_key = serialization.load_pem_private_key(f.read(), password=None)

with open('public.pem', 'rb') as f:
    public_key = serialization.load_pem_public_key(f.read())

# ๊ฐœ์ธ ํ‚ค๋กœ ์„œ๋ช…
token = jwt.encode(payload, private_key, algorithm="RS256")

# ๊ณต๊ฐœ ํ‚ค๋กœ ๊ฒ€์ฆ (๋ชจ๋“  ์„œ๋น„์Šค๊ฐ€ ํ•  ์ˆ˜ ์žˆ์Œ)
decoded = jwt.decode(token, public_key, algorithms=["RS256"])

3. ์†๋„ ์ œํ•œ

3.1 ์†๋„ ์ œํ•œ ์•Œ๊ณ ๋ฆฌ์ฆ˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    ์†๋„ ์ œํ•œ ์•Œ๊ณ ๋ฆฌ์ฆ˜                                 โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                      โ”‚
โ”‚  1. ๊ณ ์ • ์œˆ๋„์šฐ                                                      โ”‚
โ”‚     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                            โ”‚
โ”‚     โ”‚ Window 1 โ”‚โ”‚ Window 2 โ”‚โ”‚ Window 3 โ”‚                            โ”‚
โ”‚     โ”‚ โ– โ– โ– โ– โ–     โ”‚โ”‚ โ– โ– โ–       โ”‚โ”‚ โ– โ– โ– โ– โ– โ– โ–   โ”‚                           โ”‚
โ”‚     โ”‚ 5/10     โ”‚โ”‚ 3/10     โ”‚โ”‚ 7/10     โ”‚  (์ œํ•œ: ์œˆ๋„์šฐ๋‹น 10)      โ”‚
โ”‚     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                            โ”‚
โ”‚     ์žฅ์ : ๊ฐ„๋‹จ.  ๋‹จ์ : ์œˆ๋„์šฐ ๊ฒฝ๊ณ„์—์„œ ๋ฒ„์ŠคํŠธ (2๋ฐฐ ์ œํ•œ)             โ”‚
โ”‚                                                                      โ”‚
โ”‚  2. ์Šฌ๋ผ์ด๋”ฉ ์œˆ๋„์šฐ ๋กœ๊ทธ                                             โ”‚
โ”‚     Time: โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€[====ํ˜„์žฌ ์œˆ๋„์šฐ====]โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€                     โ”‚
โ”‚     ๊ฐ ์š”์ฒญ์˜ ์ •ํ™•ํ•œ ํƒ€์ž„์Šคํƒฌํ”„ ์ถ”์                                  โ”‚
โ”‚     ์Šฌ๋ผ์ด๋”ฉ ์œˆ๋„์šฐ ๋‚ด ์š”์ฒญ ์นด์šดํŠธ                                   โ”‚
โ”‚     ์žฅ์ : ์ •ํ™•.  ๋‹จ์ : ๋ฉ”๋ชจ๋ฆฌ ์ง‘์•ฝ์  (๋ชจ๋“  ํƒ€์ž„์Šคํƒฌํ”„ ์ €์žฅ)          โ”‚
โ”‚                                                                      โ”‚
โ”‚  3. ์Šฌ๋ผ์ด๋”ฉ ์œˆ๋„์šฐ ์นด์šดํ„ฐ                                           โ”‚
โ”‚     ๊ณ ์ • ์œˆ๋„์šฐ ์นด์šดํŠธ๋ฅผ ๊ฐ€์ค‘์น˜ ์˜ค๋ฒ„๋žฉ๊ณผ ๊ฒฐํ•ฉ                        โ”‚
โ”‚     weight = (window_size - elapsed) / window_size                   โ”‚
โ”‚     count = prev_count * weight + current_count                      โ”‚
โ”‚     ์žฅ์ : ๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ์ .  ๋‹จ์ : ๊ทผ์‚ฌ์น˜                               โ”‚
โ”‚                                                                      โ”‚
โ”‚  4. ํ† ํฐ ๋ฒ„ํ‚ท                                                        โ”‚
โ”‚     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                                      โ”‚
โ”‚     โ”‚ โ— โ— โ— โ— โ”‚ โ† ๋ฒ„ํ‚ท (์šฉ๋Ÿ‰: 10 ํ† ํฐ)                             โ”‚
โ”‚     โ”‚ โ— โ— โ—   โ”‚                                                     โ”‚
โ”‚     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                                      โ”‚
โ”‚         โ†‘                                                            โ”‚
โ”‚     ๋ฆฌํ•„: ์ดˆ๋‹น 1 ํ† ํฐ                                                โ”‚
โ”‚     ๊ฐ ์š”์ฒญ์€ 1 ํ† ํฐ ์†Œ๋น„                                            โ”‚
โ”‚     ์žฅ์ : ๋ฒ„์ŠคํŠธ ํ—ˆ์šฉ.  ๋‹จ์ : ๊ด€๋ฆฌํ•  ์ƒํƒœ๊ฐ€ ๋” ๋งŽ์Œ                  โ”‚
โ”‚                                                                      โ”‚
โ”‚  5. ๋ฆฌํ‚ค ๋ฒ„ํ‚ท                                                        โ”‚
โ”‚     ์š”์ฒญ์ด ํ(๋ฒ„ํ‚ท)์— ๋“ค์–ด๊ฐ                                         โ”‚
โ”‚     ๊ณ ์ • ์†๋„๋กœ ์ฒ˜๋ฆฌ (๋ˆ„์ถœ ์†๋„)                                     โ”‚
โ”‚     ์˜ค๋ฒ„ํ”Œ๋กœ๋Š” ๊ฑฐ๋ถ€๋จ                                                โ”‚
โ”‚     ์žฅ์ : ๋ถ€๋“œ๋Ÿฌ์šด ์ถœ๋ ฅ ์†๋„.  ๋‹จ์ : ์šฉ๋Ÿ‰์ด ์žˆ์–ด๋„ ์ง€์—ฐ              โ”‚
โ”‚                                                                      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

3.2 Flask-Limiter๋ฅผ ์‚ฌ์šฉํ•œ Flask ์†๋„ ์ œํ•œ

"""
Flask-Limiter๋ฅผ ์‚ฌ์šฉํ•œ ์†๋„ ์ œํ•œ ๊ตฌํ˜„.
pip install Flask-Limiter
"""
from flask import Flask, jsonify, request
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

app = Flask(__name__)

# โ”€โ”€ ๊ธฐ๋ณธ ์„ค์ • โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
limiter = Limiter(
    app=app,
    key_func=get_remote_address,       # IP ์ฃผ์†Œ๋กœ ์†๋„ ์ œํ•œ
    default_limits=["200 per day", "50 per hour"],
    storage_uri="redis://localhost:6379",  # ๋ถ„์‚ฐ์šฉ Redis ์‚ฌ์šฉ
    # storage_uri="memory://",           # ๊ฐœ๋ฐœ์šฉ ์ธ๋ฉ”๋ชจ๋ฆฌ
    strategy="fixed-window-elastic-expiry",
)


# โ”€โ”€ ์ „์—ญ ์†๋„ ์ œํ•œ (๋ชจ๋“  ๋ผ์šฐํŠธ์— ์ ์šฉ) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# ์œ„ default_limits๋ฅผ ํ†ตํ•ด ์ด๋ฏธ ์„ค์ •๋จ

# โ”€โ”€ ๋ผ์šฐํŠธ๋ณ„ ์†๋„ ์ œํ•œ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@app.route('/api/search')
@limiter.limit("10 per minute")
def search():
    """๊ฒ€์ƒ‰ ์—”๋“œํฌ์ธํŠธ โ€” ์Šคํฌ๋ž˜ํ•‘ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ ์—„๊ฒฉํ•œ ์ œํ•œ."""
    query = request.args.get('q', '')
    return jsonify({"query": query, "results": []})


@app.route('/api/login', methods=['POST'])
@limiter.limit("5 per minute")         # ๋ฌด์ฐจ๋ณ„ ๋Œ€์ž… ๋ฐฉ์ง€
def login():
    """๊ณต๊ฒฉ์ ์ธ ์†๋„ ์ œํ•œ์ด ์žˆ๋Š” ๋กœ๊ทธ์ธ ์—”๋“œํฌ์ธํŠธ."""
    return jsonify({"message": "login"})


@app.route('/api/data')
@limiter.limit("100 per hour")
@limiter.limit("10 per minute")        # ์—ฌ๋Ÿฌ ์ œํ•œ
def get_data():
    """๊ณ„์ธตํ™”๋œ ์†๋„ ์ œํ•œ์ด ์žˆ๋Š” ๋ฐ์ดํ„ฐ ์—”๋“œํฌ์ธํŠธ."""
    return jsonify({"data": []})


# โ”€โ”€ API ํ‚ค ํ‹ฐ์–ด์— ๋”ฐ๋ฅธ ๋™์  ์†๋„ ์ œํ•œ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def get_rate_limit_by_tier():
    """ํด๋ผ์ด์–ธํŠธ์˜ API ํ‹ฐ์–ด์— ๋”ฐ๋ผ ์†๋„ ์ œํ•œ ๋ฌธ์ž์—ด ๋ฐ˜ํ™˜."""
    api_key = request.headers.get('X-API-Key', '')
    # ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ํ‹ฐ์–ด ์กฐํšŒ (๋‹จ์ˆœํ™”๋จ)
    tiers = {
        "free": "100 per hour",
        "pro": "1000 per hour",
        "enterprise": "10000 per hour",
    }
    tier = get_tier_for_key(api_key)
    return tiers.get(tier, "50 per hour")  # ๊ธฐ๋ณธ๊ฐ’์€ free


@app.route('/api/premium')
@limiter.limit(get_rate_limit_by_tier)
def premium_endpoint():
    """ํ‹ฐ์–ด ๊ธฐ๋ฐ˜ ์†๋„ ์ œํ•œ์ด ์žˆ๋Š” ์—”๋“œํฌ์ธํŠธ."""
    return jsonify({"data": "premium"})


# โ”€โ”€ ์†๋„ ์ œํ•œ ํ—ค๋” โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# Flask-Limiter๊ฐ€ ์ž๋™์œผ๋กœ ์ด ํ—ค๋”๋“ค์„ ์ถ”๊ฐ€:
# X-RateLimit-Limit: 100
# X-RateLimit-Remaining: 95
# X-RateLimit-Reset: 1699999999
# Retry-After: 60 (์ œํ•œ ์ดˆ๊ณผ ์‹œ)

# โ”€โ”€ ์‚ฌ์šฉ์ž ์ •์˜ ์˜ค๋ฅ˜ ํ•ธ๋“ค๋Ÿฌ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@app.errorhandler(429)
def ratelimit_handler(e):
    """์†๋„ ์ œํ•œ ์ดˆ๊ณผ ์‹œ ์‚ฌ์šฉ์ž ์ •์˜ ์‘๋‹ต."""
    return jsonify({
        "error": "rate_limit_exceeded",
        "message": "๋„ˆ๋ฌด ๋งŽ์€ ์š”์ฒญ. ๋‚˜์ค‘์— ๋‹ค์‹œ ์‹œ๋„ํ•˜์„ธ์š”.",
        "retry_after": e.description,
    }), 429


# ํ”Œ๋ ˆ์ด์Šคํ™€๋”
def get_tier_for_key(api_key):
    return "free"

3.3 ์‚ฌ์šฉ์ž ์ •์˜ ํ† ํฐ ๋ฒ„ํ‚ท ๊ตฌํ˜„

"""
์ฒ˜์Œ๋ถ€ํ„ฐ ํ† ํฐ ๋ฒ„ํ‚ท ์†๋„ ์ œํ•œ๊ธฐ ๊ตฌํ˜„.
"""
import time
import threading
from dataclasses import dataclass, field

@dataclass
class TokenBucket:
    """ํ† ํฐ ๋ฒ„ํ‚ท ์†๋„ ์ œํ•œ๊ธฐ.

    Args:
        capacity: ๋ฒ„ํ‚ท์˜ ์ตœ๋Œ€ ํ† ํฐ ์ˆ˜
        refill_rate: ์ดˆ๋‹น ์ถ”๊ฐ€๋˜๋Š” ํ† ํฐ
    """
    capacity: int
    refill_rate: float
    tokens: float = field(init=False)
    last_refill: float = field(init=False)
    lock: threading.Lock = field(default_factory=threading.Lock, init=False)

    def __post_init__(self):
        self.tokens = float(self.capacity)
        self.last_refill = time.monotonic()

    def _refill(self):
        """๊ฒฝ๊ณผ ์‹œ๊ฐ„์— ๋”ฐ๋ผ ํ† ํฐ ์ถ”๊ฐ€."""
        now = time.monotonic()
        elapsed = now - self.last_refill
        new_tokens = elapsed * self.refill_rate
        self.tokens = min(self.capacity, self.tokens + new_tokens)
        self.last_refill = now

    def consume(self, tokens: int = 1) -> bool:
        """ํ† ํฐ ์†Œ๋น„ ์‹œ๋„. ํ—ˆ์šฉ๋˜๋ฉด True ๋ฐ˜ํ™˜."""
        with self.lock:
            self._refill()
            if self.tokens >= tokens:
                self.tokens -= tokens
                return True
            return False

    def wait_time(self) -> float:
        """์ตœ์†Œ 1๊ฐœ์˜ ํ† ํฐ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์„ ๋•Œ๊นŒ์ง€์˜ ์ดˆ ๋ฐ˜ํ™˜."""
        with self.lock:
            self._refill()
            if self.tokens >= 1:
                return 0.0
            return (1 - self.tokens) / self.refill_rate


class RateLimiterStore:
    """ํด๋ผ์ด์–ธํŠธ ํ‚ค๋ณ„ ์†๋„ ์ œํ•œ๊ธฐ ๊ด€๋ฆฌ."""

    def __init__(self, capacity: int = 100, refill_rate: float = 10.0):
        self.capacity = capacity
        self.refill_rate = refill_rate
        self._buckets: dict[str, TokenBucket] = {}
        self._lock = threading.Lock()

    def get_bucket(self, key: str) -> TokenBucket:
        """์ฃผ์–ด์ง„ ํ‚ค์— ๋Œ€ํ•œ ํ† ํฐ ๋ฒ„ํ‚ท ๊ฐ€์ ธ์˜ค๊ธฐ ๋˜๋Š” ์ƒ์„ฑ."""
        if key not in self._buckets:
            with self._lock:
                if key not in self._buckets:
                    self._buckets[key] = TokenBucket(
                        capacity=self.capacity,
                        refill_rate=self.refill_rate
                    )
        return self._buckets[key]

    def is_allowed(self, key: str, tokens: int = 1) -> bool:
        """์ฃผ์–ด์ง„ ํ‚ค์— ๋Œ€ํ•œ ์š”์ฒญ์ด ํ—ˆ์šฉ๋˜๋Š”์ง€ ํ™•์ธ."""
        bucket = self.get_bucket(key)
        return bucket.consume(tokens)


# โ”€โ”€ Flask์™€ ํ•จ๊ป˜ ์‚ฌ์šฉ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
from flask import Flask, request, jsonify
from functools import wraps

app = Flask(__name__)
rate_limiter = RateLimiterStore(capacity=100, refill_rate=10.0)

def rate_limit(capacity=100, refill_rate=10.0):
    """์‚ฌ์šฉ์ž ์ •์˜ ์†๋„ ์ œํ•œ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ."""
    store = RateLimiterStore(capacity=capacity, refill_rate=refill_rate)

    def decorator(f):
        @wraps(f)
        def decorated(*args, **kwargs):
            # IP + ์—”๋“œํฌ์ธํŠธ๋ฅผ ์†๋„ ์ œํ•œ ํ‚ค๋กœ ์‚ฌ์šฉ
            client_ip = request.remote_addr
            key = f"{client_ip}:{request.endpoint}"

            bucket = store.get_bucket(key)
            if not bucket.consume():
                wait = bucket.wait_time()
                return jsonify({
                    "error": "rate_limit_exceeded",
                    "retry_after": round(wait, 2)
                }), 429

            response = f(*args, **kwargs)
            return response
        return decorated
    return decorator


@app.route('/api/resource')
@rate_limit(capacity=10, refill_rate=1.0)  # 10 ๋ฒ„์ŠคํŠธ, ์ดˆ๋‹น 1 ์ง€์†
def get_resource():
    return jsonify({"data": "resource"})

4. ์ž…๋ ฅ ๊ฒ€์ฆ ๋ฐ ์ •์ œ

4.1 ๊ฒ€์ฆ ์•„ํ‚คํ…์ฒ˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    ์ž…๋ ฅ ๊ฒ€์ฆ ๊ณ„์ธต                                     โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                      โ”‚
โ”‚  Request โ”€โ”ฌโ”€โ”€ ๊ณ„์ธต 1: ์Šคํ‚ค๋งˆ ๊ฒ€์ฆ                                    โ”‚
โ”‚           โ”‚   (๊ตฌ์กฐ, ํƒ€์ž…, ํ•„์ˆ˜ ํ•„๋“œ)                                โ”‚
โ”‚           โ”‚                                                          โ”‚
โ”‚           โ”œโ”€โ”€ ๊ณ„์ธต 2: ๋น„์ฆˆ๋‹ˆ์Šค ๊ฒ€์ฆ                                   โ”‚
โ”‚           โ”‚   (๋ฒ”์œ„, ํ˜•์‹, ์ผ๊ด€์„ฑ)                                   โ”‚
โ”‚           โ”‚                                                          โ”‚
โ”‚           โ”œโ”€โ”€ ๊ณ„์ธต 3: ์ •์ œ                                            โ”‚
โ”‚           โ”‚   (๊ณต๋ฐฑ ์ œ๊ฑฐ, ์ •๊ทœํ™”, ์ธ์ฝ”๋”ฉ)                            โ”‚
โ”‚           โ”‚                                                          โ”‚
โ”‚           โ””โ”€โ”€ ๊ณ„์ธต 4: ๋งค๊ฐœ๋ณ€์ˆ˜ํ™”๋œ ์ž‘์—…                              โ”‚
โ”‚               (SQL ๋งค๊ฐœ๋ณ€์ˆ˜ํ™”, ํ…œํ”Œ๋ฆฟ ์ด์Šค์ผ€์ดํ”„)                    โ”‚
โ”‚                                                                      โ”‚
โ”‚  ์›์น™: ์กฐ๊ธฐ ๊ฒ€์ฆ, ๋น ๋ฅธ ์‹คํŒจ, ํด๋ผ์ด์–ธํŠธ ๋ฐ์ดํ„ฐ๋ฅผ ์ ˆ๋Œ€ ์‹ ๋ขฐํ•˜์ง€ ๋ง ๊ฒƒ. โ”‚
โ”‚                                                                      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

4.2 Marshmallow๋ฅผ ์‚ฌ์šฉํ•œ ์Šคํ‚ค๋งˆ ๊ฒ€์ฆ

"""
Marshmallow ์Šคํ‚ค๋งˆ๋ฅผ ์‚ฌ์šฉํ•œ API ์ž…๋ ฅ ๊ฒ€์ฆ.
pip install marshmallow
"""
from marshmallow import (
    Schema, fields, validate, validates, validates_schema,
    ValidationError, pre_load, RAISE
)
from flask import Flask, request, jsonify

app = Flask(__name__)


# โ”€โ”€ ์Šคํ‚ค๋งˆ ์ •์˜ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
class UserCreateSchema(Schema):
    """์ƒˆ ์‚ฌ์šฉ์ž ์ƒ์„ฑ์šฉ ์Šคํ‚ค๋งˆ."""

    class Meta:
        # ์•Œ ์ˆ˜ ์—†๋Š” ํ•„๋“œ์— ๋Œ€ํ•ด ์˜ค๋ฅ˜ ๋ฐœ์ƒ (๋Œ€๋Ÿ‰ ํ• ๋‹น ๋ฐฉ์ง€)
        unknown = RAISE

    username = fields.String(
        required=True,
        validate=[
            validate.Length(min=3, max=30),
            validate.Regexp(
                r'^[a-zA-Z0-9_]+$',
                error="์‚ฌ์šฉ์ž ์ด๋ฆ„์€ ๋ฌธ์ž, ์ˆซ์ž, ๋ฐ‘์ค„๋งŒ ํฌํ•จํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"
            ),
        ]
    )
    email = fields.Email(required=True)
    password = fields.String(
        required=True,
        load_only=True,  # ์ง๋ ฌํ™”๋œ ์ถœ๋ ฅ์— ์ ˆ๋Œ€ ํฌํ•จํ•˜์ง€ ์•Š์Œ
        validate=validate.Length(min=12, max=128),
    )
    age = fields.Integer(
        validate=validate.Range(min=13, max=150),
        load_default=None,
    )
    role = fields.String(
        validate=validate.OneOf(["user", "moderator"]),
        load_default="user",
        # ์ฐธ๊ณ : "admin"์€ API๋ฅผ ํ†ตํ•ด ํ—ˆ์šฉ๋˜์ง€ ์•Š์Œ โ€” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ํ†ตํ•ด์„œ๋งŒ
    )

    @validates('password')
    def validate_password_complexity(self, value):
        """๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณต์žก์„ฑ ์š”๊ตฌ์‚ฌํ•ญ ์ ์šฉ."""
        errors = []
        if not any(c.isupper() for c in value):
            errors.append("์ตœ์†Œ ํ•˜๋‚˜์˜ ๋Œ€๋ฌธ์ž๋ฅผ ํฌํ•จํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค")
        if not any(c.islower() for c in value):
            errors.append("์ตœ์†Œ ํ•˜๋‚˜์˜ ์†Œ๋ฌธ์ž๋ฅผ ํฌํ•จํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค")
        if not any(c.isdigit() for c in value):
            errors.append("์ตœ์†Œ ํ•˜๋‚˜์˜ ์ˆซ์ž๋ฅผ ํฌํ•จํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค")
        if not any(c in '!@#$%^&*()_+-=[]{}|;:,.<>?' for c in value):
            errors.append("์ตœ์†Œ ํ•˜๋‚˜์˜ ํŠน์ˆ˜ ๋ฌธ์ž๋ฅผ ํฌํ•จํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค")
        if errors:
            raise ValidationError(errors)

    @pre_load
    def normalize_input(self, data, **kwargs):
        """๊ฒ€์ฆ ์ „์— ์ž…๋ ฅ ๋ฐ์ดํ„ฐ ์ •๊ทœํ™”."""
        if 'email' in data:
            data['email'] = data['email'].strip().lower()
        if 'username' in data:
            data['username'] = data['username'].strip()
        return data


class SearchQuerySchema(Schema):
    """๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ์šฉ ์Šคํ‚ค๋งˆ."""

    class Meta:
        unknown = RAISE

    q = fields.String(
        required=True,
        validate=validate.Length(min=1, max=200),
    )
    page = fields.Integer(
        validate=validate.Range(min=1, max=1000),
        load_default=1,
    )
    per_page = fields.Integer(
        validate=validate.Range(min=1, max=100),
        load_default=20,
    )
    sort = fields.String(
        validate=validate.OneOf(["relevance", "date", "name"]),
        load_default="relevance",
    )
    order = fields.String(
        validate=validate.OneOf(["asc", "desc"]),
        load_default="desc",
    )


# โ”€โ”€ ๊ฒ€์ฆ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def validate_input(schema_class, location="json"):
    """์Šคํ‚ค๋งˆ์— ๋Œ€ํ•ด ์š”์ฒญ ์ž…๋ ฅ์„ ๊ฒ€์ฆํ•˜๋Š” ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ."""
    def decorator(f):
        @wraps(f)
        def decorated(*args, **kwargs):
            schema = schema_class()

            if location == "json":
                data = request.get_json(silent=True)
                if data is None:
                    return jsonify({
                        "error": "invalid_request",
                        "message": "์š”์ฒญ ๋ณธ๋ฌธ์€ ์œ ํšจํ•œ JSON์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"
                    }), 400
            elif location == "args":
                data = request.args.to_dict()
            elif location == "form":
                data = request.form.to_dict()
            else:
                raise ValueError(f"์•Œ ์ˆ˜ ์—†๋Š” ์œ„์น˜: {location}")

            try:
                validated = schema.load(data)
            except ValidationError as err:
                return jsonify({
                    "error": "validation_error",
                    "messages": err.messages,
                }), 422

            request.validated_data = validated
            return f(*args, **kwargs)
        return decorated
    return decorator


# โ”€โ”€ ๊ฒ€์ฆ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ์‚ฌ์šฉ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@app.route('/api/users', methods=['POST'])
@validate_input(UserCreateSchema, location="json")
def create_user():
    """๊ฒ€์ฆ๋œ ์ž…๋ ฅ์œผ๋กœ ์ƒˆ ์‚ฌ์šฉ์ž ์ƒ์„ฑ."""
    data = request.validated_data
    # ์ด ์‹œ์ ์—์„œ data๋Š” ์œ ํšจํ•จ์ด ๋ณด์žฅ๋จ
    return jsonify({
        "message": "์‚ฌ์šฉ์ž ์ƒ์„ฑ๋จ",
        "username": data["username"],
        "email": data["email"],
    }), 201


@app.route('/api/search')
@validate_input(SearchQuerySchema, location="args")
def search():
    """๊ฒ€์ฆ๋œ ์ฟผ๋ฆฌ ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ๊ฒ€์ƒ‰."""
    data = request.validated_data
    return jsonify({
        "query": data["q"],
        "page": data["page"],
        "per_page": data["per_page"],
    })

4.3 Pydantic ๊ฒ€์ฆ (๋Œ€์•ˆ)

"""
Pydantic v2๋ฅผ ์‚ฌ์šฉํ•œ API ๊ฒ€์ฆ.
pip install pydantic
"""
from pydantic import (
    BaseModel, Field, field_validator, model_validator,
    EmailStr, ConfigDict
)
from typing import Optional
import re

class UserCreate(BaseModel):
    """์‚ฌ์šฉ์ž ์ƒ์„ฑ์šฉ Pydantic ๋ชจ๋ธ."""
    model_config = ConfigDict(
        str_strip_whitespace=True,
        extra='forbid',  # ์•Œ ์ˆ˜ ์—†๋Š” ํ•„๋“œ ๊ฑฐ๋ถ€
    )

    username: str = Field(
        min_length=3,
        max_length=30,
        pattern=r'^[a-zA-Z0-9_]+$',
    )
    email: EmailStr
    password: str = Field(min_length=12, max_length=128)
    age: Optional[int] = Field(default=None, ge=13, le=150)
    role: str = Field(default="user", pattern=r'^(user|moderator)$')

    @field_validator('password')
    @classmethod
    def validate_password(cls, v):
        if not re.search(r'[A-Z]', v):
            raise ValueError('๋Œ€๋ฌธ์ž๋ฅผ ํฌํ•จํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค')
        if not re.search(r'[a-z]', v):
            raise ValueError('์†Œ๋ฌธ์ž๋ฅผ ํฌํ•จํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค')
        if not re.search(r'\d', v):
            raise ValueError('์ˆซ์ž๋ฅผ ํฌํ•จํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค')
        if not re.search(r'[!@#$%^&*()_+\-=\[\]{}|;:,.<>?]', v):
            raise ValueError('ํŠน์ˆ˜ ๋ฌธ์ž๋ฅผ ํฌํ•จํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค')
        return v

    @field_validator('email')
    @classmethod
    def normalize_email(cls, v):
        return v.lower()


# โ”€โ”€ Flask์—์„œ ์‚ฌ์šฉ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
from flask import Flask, request, jsonify
from pydantic import ValidationError as PydanticValidationError

app = Flask(__name__)

@app.route('/api/users', methods=['POST'])
def create_user():
    try:
        user = UserCreate(**request.get_json())
    except PydanticValidationError as e:
        return jsonify({
            "error": "validation_error",
            "details": e.errors(),
        }), 422

    return jsonify({"username": user.username}), 201

5. CORS (Cross-Origin Resource Sharing)

5.1 CORS ์ž‘๋™ ๋ฐฉ์‹

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    CORS ํ๋ฆ„                                         โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                      โ”‚
โ”‚  ๊ฐ„๋‹จํ•œ ์š”์ฒญ (GET, HEAD, ๊ฐ„๋‹จํ•œ ํ—ค๋”๊ฐ€ ์žˆ๋Š” POST):                   โ”‚
โ”‚                                                                      โ”‚
โ”‚  Browser โ”€โ”€โ”€โ”€GET /api/dataโ”€โ”€โ”€โ”€โ”€โ”€โ–ถ Server                            โ”‚
โ”‚  (origin: https://app.com)                                          โ”‚
โ”‚                                                                      โ”‚
โ”‚  Server โ”€โ”€โ”€โ”€Responseโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ Browser                           โ”‚
โ”‚  Access-Control-Allow-Origin: https://app.com                        โ”‚
โ”‚  โ”€โ”€โ”€ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์‘๋‹ต ํ—ˆ์šฉ โœ“                                         โ”‚
โ”‚                                                                      โ”‚
โ”‚  โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€       โ”‚
โ”‚                                                                      โ”‚
โ”‚  ํ”„๋ฆฌํ”Œ๋ผ์ดํŠธ ์š”์ฒญ (PUT, DELETE, ์‚ฌ์šฉ์ž ์ •์˜ ํ—ค๋”, JSON):            โ”‚
โ”‚                                                                      โ”‚
โ”‚  ๋‹จ๊ณ„ 1: ๋ธŒ๋ผ์šฐ์ €๊ฐ€ OPTIONS ํ”„๋ฆฌํ”Œ๋ผ์ดํŠธ ์ „์†ก                        โ”‚
โ”‚  Browser โ”€โ”€โ”€โ”€OPTIONS /api/dataโ”€โ”€โ”€โ–ถ Server                            โ”‚
โ”‚  Origin: https://app.com                                             โ”‚
โ”‚  Access-Control-Request-Method: PUT                                  โ”‚
โ”‚  Access-Control-Request-Headers: Content-Type, Authorization         โ”‚
โ”‚                                                                      โ”‚
โ”‚  ๋‹จ๊ณ„ 2: ์„œ๋ฒ„๊ฐ€ ํ—ˆ์šฉ๋œ ๋ฉ”์„œ๋“œ/ํ—ค๋”๋กœ ์‘๋‹ต                            โ”‚
โ”‚  Server โ”€โ”€โ”€โ”€204 No Contentโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ Browser                           โ”‚
โ”‚  Access-Control-Allow-Origin: https://app.com                        โ”‚
โ”‚  Access-Control-Allow-Methods: GET, POST, PUT, DELETE                โ”‚
โ”‚  Access-Control-Allow-Headers: Content-Type, Authorization           โ”‚
โ”‚  Access-Control-Max-Age: 86400                                       โ”‚
โ”‚                                                                      โ”‚
โ”‚  ๋‹จ๊ณ„ 3: ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์‹ค์ œ ์š”์ฒญ ์ „์†ก                                   โ”‚
โ”‚  Browser โ”€โ”€โ”€โ”€PUT /api/dataโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ Server                            โ”‚
โ”‚                                                                      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

5.2 Flask CORS ๊ตฌ์„ฑ

"""
Flask์—์„œ CORS ๊ตฌ์„ฑ.
pip install flask-cors
"""
from flask import Flask, jsonify
from flask_cors import CORS

app = Flask(__name__)

# โ”€โ”€ ์˜ต์…˜ 1: ํŠน์ • ์ถœ์ฒ˜ ํ—ˆ์šฉ (๊ถŒ์žฅ) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
CORS(app, resources={
    r"/api/*": {
        "origins": [
            "https://app.example.com",
            "https://admin.example.com",
        ],
        "methods": ["GET", "POST", "PUT", "DELETE"],
        "allow_headers": ["Content-Type", "Authorization"],
        "expose_headers": ["X-Request-Id", "X-RateLimit-Remaining"],
        "supports_credentials": True,
        "max_age": 86400,
    }
})

# โ”€โ”€ ์˜ต์…˜ 2: ๋‹ค๋ฅธ ๋ผ์šฐํŠธ์— ๋Œ€ํ•œ ๋‹ค๋ฅธ CORS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
app2 = Flask(__name__)

# ๊ณต๊ฐœ API: ๋ชจ๋“  ์ถœ์ฒ˜ ํ—ˆ์šฉ (์ž๊ฒฉ ์ฆ๋ช… ์—†์Œ)
CORS(app2, resources={
    r"/api/public/*": {
        "origins": "*",
        "methods": ["GET"],
        "allow_headers": ["Content-Type"],
        "supports_credentials": False,  # origin: *์ธ ๊ฒฝ์šฐ False์—ฌ์•ผ ํ•จ
    }
})

# ๋น„๊ณต๊ฐœ API: ์ž๊ฒฉ ์ฆ๋ช…์ด ์žˆ๋Š” ํŠน์ • ์ถœ์ฒ˜
CORS(app2, resources={
    r"/api/private/*": {
        "origins": ["https://app.example.com"],
        "methods": ["GET", "POST", "PUT", "DELETE"],
        "allow_headers": ["Content-Type", "Authorization"],
        "supports_credentials": True,
        "max_age": 86400,
    }
})

# โ”€โ”€ ์˜ต์…˜ 3: ์ˆ˜๋™ CORS ๊ตฌํ˜„ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
from flask import Flask, request, make_response

app3 = Flask(__name__)

ALLOWED_ORIGINS = {
    "https://app.example.com",
    "https://admin.example.com",
}

@app3.after_request
def add_cors_headers(response):
    origin = request.headers.get('Origin')

    if origin in ALLOWED_ORIGINS:
        response.headers['Access-Control-Allow-Origin'] = origin
        response.headers['Access-Control-Allow-Credentials'] = 'true'
        response.headers['Access-Control-Allow-Methods'] = (
            'GET, POST, PUT, DELETE, OPTIONS'
        )
        response.headers['Access-Control-Allow-Headers'] = (
            'Content-Type, Authorization, X-Requested-With'
        )
        response.headers['Access-Control-Expose-Headers'] = (
            'X-Request-Id, X-RateLimit-Remaining'
        )
        response.headers['Access-Control-Max-Age'] = '86400'
        # Vary: Origin์€ ์บ์‹œ์— ์‘๋‹ต์ด ์ถœ์ฒ˜์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์ง„๋‹ค๊ณ  ์•Œ๋ฆผ
        response.headers.add('Vary', 'Origin')

    return response

@app3.route('/api/data', methods=['OPTIONS'])
def preflight():
    """CORS ํ”„๋ฆฌํ”Œ๋ผ์ดํŠธ ์š”์ฒญ ์ฒ˜๋ฆฌ."""
    return make_response('', 204)


# โ”€โ”€ CORS ๋ณด์•ˆ ๊ทœ์น™ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
"""
1. ์ž๊ฒฉ ์ฆ๋ช…๊ณผ ํ•จ๊ป˜ Access-Control-Allow-Origin: *๋ฅผ ์ ˆ๋Œ€ ์‚ฌ์šฉํ•˜์ง€ ๋ง ๊ฒƒ
   - ์‹ค์ œ๋กœ ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ฐจ๋‹จ๋จ
   - ์ž๊ฒฉ ์ฆ๋ช…์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ ํŠน์ • ์ถœ์ฒ˜ ๋‚˜์—ด

2. ํ™•์ธ ์—†์ด Origin ํ—ค๋”๋ฅผ Allow-Origin์œผ๋กœ ์ ˆ๋Œ€ ๋ฐ˜์˜ํ•˜์ง€ ๋ง ๊ฒƒ
   - ์ด๋Š” ๋ชจ๋“  ์ถœ์ฒ˜๋ฅผ ํ—ˆ์šฉํ•˜๋Š” ๊ฒƒ๊ณผ ๋™์ผ
   - ๋‚˜์จ:  response.headers['ACAO'] = request.headers['Origin']
   - ์ข‹์Œ: ๋จผ์ € ํ—ˆ์šฉ ๋ชฉ๋ก์— ๋Œ€ํ•ด ํ™•์ธ

3. Access-Control-Allow-Methods๋ฅผ ์‹ค์ œ๋กœ ํ•„์š”ํ•œ ๊ฒƒ์œผ๋กœ ์ œํ•œ
   - ๋ผ์šฐํŠธ๊ฐ€ ์ง€์›ํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ DELETE๋ฅผ ํ—ˆ์šฉํ•˜์ง€ ๋ง ๊ฒƒ

4. ํ”„๋ฆฌํ”Œ๋ผ์ดํŠธ ์š”์ฒญ์„ ์ค„์ด๊ธฐ ์œ„ํ•ด Access-Control-Max-Age ์„ค์ •
   - 86400 (24์‹œ๊ฐ„)์ด ํ•ฉ๋ฆฌ์ 

5. ์‚ฌ์šฉ์ž ์ •์˜ ํ—ค๋”์— Access-Control-Expose-Headers ์‚ฌ์šฉ
   - ๊ธฐ๋ณธ์ ์œผ๋กœ ๊ฐ„๋‹จํ•œ ํ—ค๋”๋งŒ JavaScript์—์„œ ์ฝ์„ ์ˆ˜ ์žˆ์Œ

6. ACAO๊ฐ€ ์š”์ฒญ๋ณ„๋กœ ๋ณ€๊ฒฝ๋˜๋Š” ๊ฒฝ์šฐ ํ•ญ์ƒ Vary: Origin ์ถ”๊ฐ€
   - ์บ์‹œ ํฌ์ด์ฆˆ๋‹ ๋ฐฉ์ง€
"""

6. GraphQL ๋ณด์•ˆ

6.1 GraphQL ํŠน์ • ์œ„ํ˜‘

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    GraphQL ๋ณด์•ˆ ์œ„ํ˜‘                                 โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                      โ”‚
โ”‚  1. ์ฟผ๋ฆฌ ๊นŠ์ด ๊ณต๊ฒฉ                                                   โ”‚
โ”‚     query { user { posts { comments { author { posts { ... } } } } }โ”‚
โ”‚     โ”€โ”€โ–ถ ๊นŠ์ด ์ค‘์ฒฉ ์ฟผ๋ฆฌ๊ฐ€ ๊ธฐํ•˜๊ธ‰์ˆ˜์  DB ๋ถ€ํ•˜ ์œ ๋ฐœ                     โ”‚
โ”‚                                                                      โ”‚
โ”‚  2. ์ฟผ๋ฆฌ ํญ ๊ณต๊ฒฉ                                                     โ”‚
โ”‚     query { user1: user(id:1) {...} user2: user(id:2) {...} ... }   โ”‚
โ”‚     โ”€โ”€โ–ถ ๋งŽ์€ ๋ณ„์นญ์ด ์ฟผ๋ฆฌ ๋น„์šฉ์„ ๊ณฑํ•จ                                โ”‚
โ”‚                                                                      โ”‚
โ”‚  3. ์ธํŠธ๋กœ์ŠคํŽ™์…˜ ๋‚จ์šฉ                                                โ”‚
โ”‚     query { __schema { types { name fields { name } } } }           โ”‚
โ”‚     โ”€โ”€โ–ถ ๊ณต๊ฒฉ์ž์—๊ฒŒ ์ „์ฒด API ์Šคํ‚ค๋งˆ ๋…ธ์ถœ                              โ”‚
โ”‚                                                                      โ”‚
โ”‚  4. ๋ฐฐ์น˜ ๊ณต๊ฒฉ                                                        โ”‚
โ”‚     [{"query": "..."}, {"query": "..."}, ... x 1000]                โ”‚
โ”‚     โ”€โ”€โ–ถ ๋‹จ์ผ ์š”์ฒญ์— ์—ฌ๋Ÿฌ ์ฟผ๋ฆฌ                                        โ”‚
โ”‚                                                                      โ”‚
โ”‚  5. ๋ณ€์ˆ˜๋ฅผ ํ†ตํ•œ ์ฃผ์ž…                                                 โ”‚
โ”‚     query ($id: String!) { user(id: $id) { ... } }                  โ”‚
โ”‚     variables: { "id": "1 OR 1=1" }                                 โ”‚
โ”‚     โ”€โ”€โ–ถ ๊ฒ€์ฆ๋˜์ง€ ์•Š์€ ๋ณ€์ˆ˜๋ฅผ ํ†ตํ•œ SQL ์ฃผ์ž…                           โ”‚
โ”‚                                                                      โ”‚
โ”‚  6. ์ •๋ณด ๋…ธ์ถœ                                                        โ”‚
โ”‚     ๋‚ด๋ถ€ ์„ธ๋ถ€ ์ •๋ณด๋ฅผ ๋“œ๋Ÿฌ๋‚ด๋Š” ์ž์„ธํ•œ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€                     โ”‚
โ”‚     ํ•„๋“œ ์ œ์•ˆ: "secretAdminField๋ฅผ ์˜๋ฏธํ•˜์…จ๋‚˜์š”?"                    โ”‚
โ”‚                                                                      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

6.2 GraphQL ๋ณด์•ˆ ์™„ํ™”

"""
graphene (Python)์„ ์‚ฌ์šฉํ•œ GraphQL ๋ณด์•ˆ ์กฐ์น˜.
pip install graphene flask-graphql
"""

# โ”€โ”€ 1. ์ฟผ๋ฆฌ ๊นŠ์ด ์ œํ•œ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
class DepthAnalyzer:
    """GraphQL ์ฟผ๋ฆฌ ๊นŠ์ด ๋ถ„์„ ๋ฐ ์ œํ•œ."""

    def __init__(self, max_depth: int = 10):
        self.max_depth = max_depth

    def analyze(self, query_ast, depth: int = 0) -> int:
        """์ฟผ๋ฆฌ์˜ ์ตœ๋Œ€ ๊นŠ์ด ๊ณ„์‚ฐ."""
        if depth > self.max_depth:
            raise ValueError(
                f"์ฟผ๋ฆฌ ๊นŠ์ด {depth}๊ฐ€ ํ—ˆ์šฉ๋œ ์ตœ๋Œ€๊ฐ’({self.max_depth})์„ ์ดˆ๊ณผํ•ฉ๋‹ˆ๋‹ค"
            )

        max_child_depth = depth
        if hasattr(query_ast, 'selection_set') and query_ast.selection_set:
            for selection in query_ast.selection_set.selections:
                child_depth = self.analyze(selection, depth + 1)
                max_child_depth = max(max_child_depth, child_depth)

        return max_child_depth


# โ”€โ”€ 2. ์ฟผ๋ฆฌ ๋น„์šฉ ๋ถ„์„ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
class QueryCostAnalyzer:
    """GraphQL ์ฟผ๋ฆฌ์˜ ๋น„์šฉ ์ถ”์ •."""

    # ํ•„๋“œ ์œ ํ˜•๋ณ„ ๋น„์šฉ ์ •์˜
    FIELD_COSTS = {
        "user": 1,
        "posts": 5,       # ๋ฆฌ์ŠคํŠธ ํ•„๋“œ, ์ž ์žฌ์ ์œผ๋กœ ๋น„์Œˆ
        "comments": 3,
        "search": 10,     # ์ „์ฒด ํ…์ŠคํŠธ ๊ฒ€์ƒ‰์€ ๋น„์Œˆ
    }

    def __init__(self, max_cost: int = 1000):
        self.max_cost = max_cost

    def calculate_cost(self, query_ast, multiplier: int = 1) -> int:
        """์˜ˆ์ƒ ์ฟผ๋ฆฌ ๋น„์šฉ ๊ณ„์‚ฐ."""
        total_cost = 0

        if hasattr(query_ast, 'selection_set') and query_ast.selection_set:
            for selection in query_ast.selection_set.selections:
                field_name = selection.name.value
                field_cost = self.FIELD_COSTS.get(field_name, 1)

                # ๋น„์šฉ์„ ๊ณฑํ•˜๋Š” ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ธ์ˆ˜ ํ™•์ธ
                args = {
                    arg.name.value: arg.value.value
                    for arg in (selection.arguments or [])
                    if hasattr(arg.value, 'value')
                }
                limit = int(args.get('first', args.get('limit', 1)))

                cost = field_cost * multiplier * max(limit, 1)
                total_cost += cost

                # ์ž์‹ ์„ ํƒ์œผ๋กœ ์žฌ๊ท€
                total_cost += self.calculate_cost(selection, limit)

        if total_cost > self.max_cost:
            raise ValueError(
                f"์ฟผ๋ฆฌ ๋น„์šฉ {total_cost}๊ฐ€ ์ตœ๋Œ€๊ฐ’({self.max_cost})์„ ์ดˆ๊ณผํ•ฉ๋‹ˆ๋‹ค"
            )

        return total_cost


# โ”€โ”€ 3. ํ”„๋กœ๋•์…˜์—์„œ ์ธํŠธ๋กœ์ŠคํŽ™์…˜ ๋น„ํ™œ์„ฑํ™” โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
"""
์ธํŠธ๋กœ์ŠคํŽ™์…˜์€ ์ „์ฒด API ์Šคํ‚ค๋งˆ๋ฅผ ๋“œ๋Ÿฌ๋ƒ…๋‹ˆ๋‹ค.
ํ”„๋กœ๋•์…˜์—์„œ๋Š” ๋น„ํ™œ์„ฑํ™”ํ•˜์„ธ์š”.
"""
from graphql import GraphQLError

class DisableIntrospection:
    """ํ”„๋กœ๋•์…˜์—์„œ ์ธํŠธ๋กœ์ŠคํŽ™์…˜ ์ฟผ๋ฆฌ๋ฅผ ๋น„ํ™œ์„ฑํ™”ํ•˜๋Š” ๋ฏธ๋“ค์›จ์–ด."""

    def resolve(self, next, root, info, **kwargs):
        # __schema ๋ฐ __type ์ฟผ๋ฆฌ ์ฐจ๋‹จ
        if info.field_name in ('__schema', '__type'):
            raise GraphQLError("์ธํŠธ๋กœ์ŠคํŽ™์…˜์ด ๋น„ํ™œ์„ฑํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค")
        return next(root, info, **kwargs)


# โ”€โ”€ 4. ์†๋„ ์ œํ•œ + ๋ฐฐ์น˜ ์ œํ•œ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
from flask import Flask, request, jsonify

app = Flask(__name__)

MAX_BATCH_SIZE = 5  # ๋ฐฐ์น˜ ์š”์ฒญ๋‹น ์ตœ๋Œ€ ์ฟผ๋ฆฌ ์ˆ˜

@app.before_request
def limit_batch_queries():
    """๋ฐฐ์น˜ ์š”์ฒญ์˜ ์ฟผ๋ฆฌ ์ˆ˜ ์ œํ•œ."""
    if request.is_json:
        data = request.get_json(silent=True)
        if isinstance(data, list):
            if len(data) > MAX_BATCH_SIZE:
                return jsonify({
                    "error": "batch_limit_exceeded",
                    "message": f"๋ฐฐ์น˜๋‹น ์ตœ๋Œ€ {MAX_BATCH_SIZE}๊ฐœ ์ฟผ๋ฆฌ"
                }), 400


# โ”€โ”€ 5. ์ง€์† ์ฟผ๋ฆฌ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
"""
์ž„์˜์˜ ์ฟผ๋ฆฌ๋ฅผ ์ˆ˜๋ฝํ•˜๋Š” ๋Œ€์‹ , ์‚ฌ์ „ ๋“ฑ๋ก๋œ
์ฟผ๋ฆฌ ํ•ด์‹œ๋งŒ ์ˆ˜๋ฝํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ์ฃผ์ž… ๋ฐ ์ฟผ๋ฆฌ ์กฐ์ž‘์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.
"""
import hashlib

# ์‚ฌ์ „ ๋“ฑ๋ก๋œ ์ฟผ๋ฆฌ (๋นŒ๋“œ ํƒ€์ž„์— ์ƒ์„ฑ)
PERSISTED_QUERIES = {
    "abc123": "query { users { id name } }",
    "def456": "query GetUser($id: ID!) { user(id: $id) { id name email } }",
}

@app.route('/graphql', methods=['POST'])
def graphql_endpoint():
    data = request.get_json()

    # ํ”„๋กœ๋•์…˜์—์„œ๋Š” ์ง€์† ์ฟผ๋ฆฌ๋งŒ ์ˆ˜๋ฝ
    query_hash = data.get('extensions', {}).get('persistedQuery', {}).get('sha256Hash')

    if query_hash:
        query = PERSISTED_QUERIES.get(query_hash)
        if not query:
            return jsonify({"error": "query_not_found"}), 404
    else:
        # ๊ฐœ๋ฐœ์—์„œ๋Š” ์ž„์˜์˜ ์ฟผ๋ฆฌ ํ—ˆ์šฉ
        # ํ”„๋กœ๋•์…˜์—์„œ๋Š” ๊ฑฐ๋ถ€:
        return jsonify({
            "error": "persisted_queries_only",
            "message": "์ง€์† ์ฟผ๋ฆฌ๋งŒ ์ˆ˜๋ฝ๋ฉ๋‹ˆ๋‹ค"
        }), 400

    # ์ฟผ๋ฆฌ ์‹คํ–‰...
    return jsonify({"data": {}})

7. API ๊ฒŒ์ดํŠธ์›จ์ด ๋ณด์•ˆ

7.1 ๊ฒŒ์ดํŠธ์›จ์ด ์•„ํ‚คํ…์ฒ˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    API ๊ฒŒ์ดํŠธ์›จ์ด ๋ณด์•ˆ ์•„ํ‚คํ…์ฒ˜                      โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                      โ”‚
โ”‚  Client                                                              โ”‚
โ”‚    โ”‚                                                                 โ”‚
โ”‚    โ–ผ                                                                 โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                  โ”‚
โ”‚  โ”‚              API ๊ฒŒ์ดํŠธ์›จ์ด                      โ”‚                 โ”‚
โ”‚  โ”‚                                                 โ”‚                 โ”‚
โ”‚  โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”       โ”‚                 โ”‚
โ”‚  โ”‚  โ”‚   TLS    โ”‚ โ”‚   ์ธ์ฆ   โ”‚ โ”‚  ์†๋„    โ”‚       โ”‚                 โ”‚
โ”‚  โ”‚  โ”‚  ์ข…๋ฃŒ    โ”‚ โ”‚  ๊ฒ€์ฆ    โ”‚ โ”‚  ์ œํ•œ    โ”‚       โ”‚                 โ”‚
โ”‚  โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜       โ”‚                 โ”‚
โ”‚  โ”‚                                                 โ”‚                 โ”‚
โ”‚  โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”       โ”‚                 โ”‚
โ”‚  โ”‚  โ”‚  ์ž…๋ ฅ    โ”‚ โ”‚  ์š”์ฒญ    โ”‚ โ”‚  CORS    โ”‚       โ”‚                 โ”‚
โ”‚  โ”‚  โ”‚  ๊ฒ€์ฆ    โ”‚ โ”‚  ๋กœ๊น…    โ”‚ โ”‚  ์ฒ˜๋ฆฌ    โ”‚       โ”‚                 โ”‚
โ”‚  โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜       โ”‚                 โ”‚
โ”‚  โ”‚                                                 โ”‚                 โ”‚
โ”‚  โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”       โ”‚                 โ”‚
โ”‚  โ”‚  โ”‚  WAF     โ”‚ โ”‚ IP ํ—ˆ์šฉ/ โ”‚ โ”‚  ์‘๋‹ต    โ”‚       โ”‚                 โ”‚
โ”‚  โ”‚  โ”‚  ๊ทœ์น™    โ”‚ โ”‚ ์ฐจ๋‹จ ๋ชฉ๋กโ”‚ โ”‚  ํ•„ํ„ฐ๋ง  โ”‚       โ”‚                 โ”‚
โ”‚  โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜       โ”‚                 โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                  โ”‚
โ”‚           โ”‚              โ”‚              โ”‚                             โ”‚
โ”‚           โ–ผ              โ–ผ              โ–ผ                             โ”‚
โ”‚    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                       โ”‚
โ”‚    โ”‚์„œ๋น„์Šค A  โ”‚   โ”‚์„œ๋น„์Šค B  โ”‚   โ”‚์„œ๋น„์Šค C  โ”‚                       โ”‚
โ”‚    โ”‚(์‚ฌ์šฉ์ž)  โ”‚   โ”‚(์ฃผ๋ฌธ)    โ”‚   โ”‚(๊ฒ€์ƒ‰)    โ”‚                       โ”‚
โ”‚    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                       โ”‚
โ”‚                                                                      โ”‚
โ”‚  ์ค‘์•™ ๊ฒŒ์ดํŠธ์›จ์ด์˜ ๋ณด์•ˆ ์ด์ :                                        โ”‚
โ”‚  โ€ข ์ธ์ฆ ์ ์šฉ์„ ์œ„ํ•œ ๋‹จ์ผ ์ง€์                                         โ”‚
โ”‚  โ€ข ์„œ๋น„์Šค ์ „๋ฐ˜์— ๊ฑธ์นœ ์ผ๊ด€๋œ ์†๋„ ์ œํ•œ                               โ”‚
โ”‚  โ€ข ์ค‘์•™ ์ง‘์ค‘์‹ ๋กœ๊น… ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง                                      โ”‚
โ”‚  โ€ข ๋ฐฑ์—”๋“œ ์„œ๋น„์Šค๊ฐ€ TLS๋ฅผ ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š์Œ                               โ”‚
โ”‚  โ€ข ๋‹จ์ˆœํ™”๋œ ๋ณด์•ˆ ์ •์ฑ… ๊ด€๋ฆฌ                                           โ”‚
โ”‚                                                                      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

7.2 ์š”์ฒญ/์‘๋‹ต ํ•„ํ„ฐ๋ง

"""
API ๊ฒŒ์ดํŠธ์›จ์ด ์š”์ฒญ/์‘๋‹ต ํ•„ํ„ฐ๋ง ๋ฏธ๋“ค์›จ์–ด.
"""
from flask import Flask, request, jsonify, g
import re
import uuid
import time
import logging

app = Flask(__name__)
logger = logging.getLogger('api_gateway')


# โ”€โ”€ ์š”์ฒญ ID ์ถ”์  โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@app.before_request
def add_request_id():
    """์ถ”์ ์„ ์œ„ํ•œ ๊ณ ์œ  ์š”์ฒญ ID ์ถ”๊ฐ€."""
    g.request_id = request.headers.get(
        'X-Request-Id',
        str(uuid.uuid4())
    )
    g.request_start = time.monotonic()


@app.after_request
def add_response_headers(response):
    """์‘๋‹ต์— ๋ณด์•ˆ ๋ฐ ์ถ”์  ํ—ค๋” ์ถ”๊ฐ€."""
    response.headers['X-Request-Id'] = g.request_id
    # ํƒ€์ด๋ฐ ์ถ”๊ฐ€
    elapsed = time.monotonic() - g.request_start
    response.headers['X-Response-Time'] = f"{elapsed:.3f}s"
    return response


# โ”€โ”€ ์š”์ฒญ ํฌ๊ธฐ ์ œํ•œ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
MAX_CONTENT_LENGTH = 1 * 1024 * 1024  # 1 MB
app.config['MAX_CONTENT_LENGTH'] = MAX_CONTENT_LENGTH

@app.before_request
def check_content_length():
    """๊ณผ๋„ํ•œ ํฌ๊ธฐ์˜ ์š”์ฒญ ๊ฑฐ๋ถ€."""
    content_length = request.content_length
    if content_length and content_length > MAX_CONTENT_LENGTH:
        return jsonify({
            "error": "payload_too_large",
            "max_bytes": MAX_CONTENT_LENGTH,
        }), 413


# โ”€โ”€ IP ํ—ˆ์šฉ ๋ชฉ๋ก/์ฐจ๋‹จ ๋ชฉ๋ก โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
BLOCKED_IPS = {"192.168.1.100", "10.0.0.50"}
ADMIN_ALLOWED_IPS = {"10.0.0.1", "10.0.0.2"}

@app.before_request
def check_ip():
    """๊ธˆ์ง€๋œ IP๋กœ๋ถ€ํ„ฐ์˜ ์š”์ฒญ ์ฐจ๋‹จ."""
    client_ip = request.remote_addr

    if client_ip in BLOCKED_IPS:
        logger.warning(f"๊ธˆ์ง€๋œ IP์—์„œ ์š”์ฒญ ์ฐจ๋‹จ: {client_ip}")
        return jsonify({"error": "forbidden"}), 403

    # ๊ด€๋ฆฌ์ž ๋ผ์šฐํŠธ๋Š” ํŠน์ • IP ํ•„์š”
    if request.path.startswith('/admin/'):
        if client_ip not in ADMIN_ALLOWED_IPS:
            return jsonify({"error": "forbidden"}), 403


# โ”€โ”€ ์‘๋‹ต ๋ฐ์ดํ„ฐ ํ•„ํ„ฐ๋ง โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
SENSITIVE_FIELDS = {'password', 'ssn', 'credit_card', 'secret_key',
                    'token', 'api_key'}

def filter_sensitive_data(data):
    """์‘๋‹ต ๋ฐ์ดํ„ฐ์—์„œ ๋ฏผ๊ฐํ•œ ํ•„๋“œ๋ฅผ ์žฌ๊ท€์ ์œผ๋กœ ์ œ๊ฑฐ."""
    if isinstance(data, dict):
        return {
            k: filter_sensitive_data(v)
            for k, v in data.items()
            if k.lower() not in SENSITIVE_FIELDS
        }
    elif isinstance(data, list):
        return [filter_sensitive_data(item) for item in data]
    return data


# โ”€โ”€ ๊ฐ์‚ฌ ๋กœ๊น… โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@app.after_request
def audit_log(response):
    """๊ฐ์‚ฌ ์ถ”์ ์„ ์œ„ํ•œ ๋ชจ๋“  API ์š”์ฒญ ๋กœ๊ทธ."""
    logger.info(
        "API ์š”์ฒญ: method=%s path=%s status=%s "
        "ip=%s user_agent=%s request_id=%s duration=%s",
        request.method,
        request.path,
        response.status_code,
        request.remote_addr,
        request.user_agent.string[:100],
        g.request_id,
        response.headers.get('X-Response-Time', 'N/A'),
    )
    return response

8. OpenAPI ๋ณด์•ˆ ์ •์˜

8.1 OpenAPI 3.0์˜ ๋ณด์•ˆ ์Šคํ‚ด

# openapi.yaml - ๋ณด์•ˆ ์Šคํ‚ด ์ •์˜
openapi: 3.0.3
info:
  title: Secure API
  version: 1.0.0

# โ”€โ”€ ๋ณด์•ˆ ์Šคํ‚ด ์ •์˜ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
components:
  securitySchemes:
    # API ํ‚ค ์ธ์ฆ
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: ์„œ๋ฒ„ ๊ฐ„ ํ†ต์‹ ์„ ์œ„ํ•œ API ํ‚ค

    # JWT Bearer ํ† ํฐ
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: JWT ์•ก์„ธ์Šค ํ† ํฐ

    # OAuth 2.0
    OAuth2:
      type: oauth2
      flows:
        authorizationCode:
          authorizationUrl: https://auth.example.com/authorize
          tokenUrl: https://auth.example.com/token
          refreshUrl: https://auth.example.com/refresh
          scopes:
            read: ๋ฆฌ์†Œ์Šค ์ฝ๊ธฐ ์ ‘๊ทผ
            write: ๋ฆฌ์†Œ์Šค ์“ฐ๊ธฐ ์ ‘๊ทผ
            admin: ๊ด€๋ฆฌ ์ ‘๊ทผ

    # OpenID Connect
    OpenIdConnect:
      type: openIdConnect
      openIdConnectUrl: https://auth.example.com/.well-known/openid-configuration

# โ”€โ”€ ์ „์—ญ ๋ณด์•ˆ (๋ชจ๋“  ์—”๋“œํฌ์ธํŠธ์— ์ ์šฉ) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
security:
  - BearerAuth: []

# โ”€โ”€ ์—”๋“œํฌ์ธํŠธ๋ณ„ ๋ณด์•ˆ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
paths:
  /api/public/status:
    get:
      summary: ์ƒํƒœ ํ™•์ธ (์ธ์ฆ ๋ถˆํ•„์š”)
      security: []  # ์žฌ์ •์˜: ์ธ์ฆ ์—†์Œ
      responses:
        '200':
          description: OK

  /api/users:
    get:
      summary: ์‚ฌ์šฉ์ž ๋ชฉ๋ก
      security:
        - BearerAuth: []
        - ApiKeyAuth: []  # ๋Œ€์•ˆ ์ธ์ฆ (OR)
      responses:
        '200':
          description: ์‚ฌ์šฉ์ž ๋ชฉ๋ก

  /api/admin/settings:
    put:
      summary: ์„ค์ • ์—…๋ฐ์ดํŠธ (๊ด€๋ฆฌ์ž๋งŒ)
      security:
        - OAuth2: [admin]  # 'admin' ๋ฒ”์œ„ ํ•„์š”
      responses:
        '200':
          description: ์„ค์ • ์—…๋ฐ์ดํŠธ๋จ

  /api/data:
    post:
      summary: ๋ฐ์ดํ„ฐ ์ƒ์„ฑ (read + write ํ•„์š”)
      security:
        - OAuth2: [read, write]  # ๋‘ ๋ฒ”์œ„ ๋ชจ๋‘ ํ•„์š”
      responses:
        '201':
          description: ๋ฐ์ดํ„ฐ ์ƒ์„ฑ๋จ

8.2 API ๋ฒ„์ „ ๊ด€๋ฆฌ ๋ณด์•ˆ

"""
๋ณด์•ˆ์„ ์œ„ํ•œ API ๋ฒ„์ „ ๊ด€๋ฆฌ ๊ณ ๋ ค ์‚ฌํ•ญ.
"""

# โ”€โ”€ URL ๊ฒฝ๋กœ ๋ฒ„์ „ ๊ด€๋ฆฌ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# /api/v1/users  (๊ตฌ๋ฒ„์ „, ์•Œ๋ ค์ง„ ์ทจ์•ฝ์ ์ด ์žˆ์„ ์ˆ˜ ์žˆ์Œ)
# /api/v2/users  (ํ˜„์žฌ, ํŒจ์น˜๋จ)

# โ”€โ”€ ํ—ค๋” ๋ฒ„์ „ ๊ด€๋ฆฌ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# Accept: application/vnd.example.v2+json

# โ”€โ”€ ๋ฒ„์ „ ๊ด€๋ฆฌ์™€ ๊ด€๋ จ๋œ ๋ณด์•ˆ ๋ฌธ์ œ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
"""
1. ๊ตฌ API ๋ฒ„์ „์—๋Š” ์•Œ๋ ค์ง„ ์ทจ์•ฝ์ ์ด ์žˆ์„ ์ˆ˜ ์žˆ์Œ
   - ํ๊ธฐ ๋‚ ์งœ๋ฅผ ์„ค์ •ํ•˜๊ณ  ์ ์šฉ
   - Deprecation ๋ฐ Sunset ํ—ค๋” ๋ฐ˜ํ™˜

2. ํ๊ธฐ๋œ ๋ฒ„์ „์— ๋Œ€ํ•ด ๋ณด์•ˆ ํŒจ์น˜๋ฅผ ์œ ์ง€ํ•˜์ง€ ๋ง ๊ฒƒ
   - ์ตœ์‹  ๋ฒ„์ „์œผ๋กœ ๊ฐ•์ œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜

3. ํ๊ธฐ๋œ ๋ฒ„์ „์˜ ์‚ฌ์šฉ ๋ชจ๋‹ˆํ„ฐ๋ง
   - ๊ตฌ๋ฒ„์ „์ด ์—ฌ์ „ํžˆ ์‚ฌ์šฉ ์ค‘์ผ ๋•Œ ๊ฒฝ๊ณ 
"""

from flask import Flask, request, jsonify
from datetime import datetime

app = Flask(__name__)

API_VERSIONS = {
    "v1": {
        "status": "deprecated",
        "sunset": "2025-06-01",
        "successor": "v2",
    },
    "v2": {
        "status": "current",
        "sunset": None,
        "successor": None,
    },
}

@app.before_request
def check_api_version():
    """ํ๊ธฐ๋œ API ๋ฒ„์ „ ๊ฒฝ๊ณ  ๋˜๋Š” ์ฐจ๋‹จ."""
    # URL ๊ฒฝ๋กœ์—์„œ ๋ฒ„์ „ ์ถ”์ถœ
    path_parts = request.path.strip('/').split('/')
    if len(path_parts) >= 2 and path_parts[0] == 'api':
        version = path_parts[1]
        version_info = API_VERSIONS.get(version)

        if not version_info:
            return jsonify({
                "error": "invalid_version",
                "supported": list(API_VERSIONS.keys()),
            }), 400

        if version_info["status"] == "deprecated":
            sunset_date = version_info["sunset"]
            if sunset_date:
                # ์ผ๋ชฐ ๋‚ ์งœ๊ฐ€ ์ง€๋‚ฌ๋Š”์ง€ ํ™•์ธ
                if datetime.now() > datetime.fromisoformat(sunset_date):
                    return jsonify({
                        "error": "version_retired",
                        "message": f"API {version}์€(๋Š”) {sunset_date}์— ํ๊ธฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค",
                        "upgrade_to": version_info["successor"],
                    }), 410  # 410 Gone


@app.after_request
def add_deprecation_headers(response):
    """์‘๋‹ต ํ—ค๋”์— ํ๊ธฐ ๊ฒฝ๊ณ  ์ถ”๊ฐ€."""
    path_parts = request.path.strip('/').split('/')
    if len(path_parts) >= 2 and path_parts[0] == 'api':
        version = path_parts[1]
        version_info = API_VERSIONS.get(version, {})

        if version_info.get("status") == "deprecated":
            response.headers['Deprecation'] = 'true'
            if version_info.get("sunset"):
                response.headers['Sunset'] = version_info["sunset"]
            response.headers['Link'] = (
                f'</api/{version_info["successor"]}>; rel="successor-version"'
            )

    return response

9. ์š”์ฒญ/์‘๋‹ต ์•”ํ˜ธํ™”

9.1 ์ „์†ก ๊ณ„์ธต ๋ณด์•ˆ

"""
TLS ๊ตฌ์„ฑ ๋ฐ ์ธ์ฆ์„œ ๊ณ ์ •.
"""

# โ”€โ”€ TLS๋ฅผ ์‚ฌ์šฉํ•œ Flask (๊ฐœ๋ฐœ) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# ๊ฐœ๋ฐœ์šฉ ์ž์ฒด ์„œ๋ช… ์ธ์ฆ์„œ ์ƒ์„ฑ:
# openssl req -x509 -newkey rsa:4096 -nodes \
#   -out cert.pem -keyout key.pem -days 365

from flask import Flask
app = Flask(__name__)

# TLS๋กœ ์‹คํ–‰ (๊ฐœ๋ฐœ ์ „์šฉ)
# ํ”„๋กœ๋•์…˜์—์„œ๋Š” TLS๊ฐ€ ์—ญ๋ฐฉํ–ฅ ํ”„๋ก์‹œ(nginx, ๋กœ๋“œ ๋ฐธ๋Ÿฐ์„œ)์—์„œ ์ฒ˜๋ฆฌ๋จ
if __name__ == '__main__':
    app.run(
        ssl_context=('cert.pem', 'key.pem'),
        host='0.0.0.0',
        port=443,
    )

# โ”€โ”€ ์ธ์ฆ์„œ ๊ณ ์ • (API ํด๋ผ์ด์–ธํŠธ์šฉ) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
import requests
import hashlib
import ssl

def verify_certificate_pin(host: str, expected_pin: str) -> bool:
    """์„œ๋ฒ„ ์ธ์ฆ์„œ๊ฐ€ ์˜ˆ์ƒ ํ•€๊ณผ ์ผ์น˜ํ•˜๋Š”์ง€ ํ™•์ธ."""
    import socket

    context = ssl.create_default_context()
    with socket.create_connection((host, 443)) as sock:
        with context.wrap_socket(sock, server_hostname=host) as ssock:
            cert_der = ssock.getpeercert(True)
            cert_hash = hashlib.sha256(cert_der).hexdigest()
            return cert_hash == expected_pin


# โ”€โ”€ ํŽ˜์ด๋กœ๋“œ ์ˆ˜์ค€ ์•”ํ˜ธํ™” (๋ฏผ๊ฐํ•œ ํ•„๋“œ์šฉ) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
from cryptography.fernet import Fernet

# ํ‚ค ์ƒ์„ฑ (์ฝ”๋“œ๊ฐ€ ์•„๋‹Œ ์•ˆ์ „ํ•˜๊ฒŒ ์ €์žฅ)
encryption_key = Fernet.generate_key()
cipher = Fernet(encryption_key)

def encrypt_sensitive_fields(data: dict, fields: list[str]) -> dict:
    """์‘๋‹ต ํŽ˜์ด๋กœ๋“œ์—์„œ ํŠน์ • ํ•„๋“œ ์•”ํ˜ธํ™”."""
    result = data.copy()
    for field in fields:
        if field in result:
            value = str(result[field]).encode()
            result[field] = cipher.encrypt(value).decode()
    return result

def decrypt_sensitive_fields(data: dict, fields: list[str]) -> dict:
    """์š”์ฒญ ํŽ˜์ด๋กœ๋“œ์—์„œ ํŠน์ • ํ•„๋“œ ๋ณตํ˜ธํ™”."""
    result = data.copy()
    for field in fields:
        if field in result:
            value = result[field].encode()
            result[field] = cipher.decrypt(value).decode()
    return result

10. ์—ฐ์Šต ๋ฌธ์ œ

์—ฐ์Šต 1: ์•ˆ์ „ํ•œ JWT ์ธ์ฆ ์„œ๋น„์Šค

๋‹ค์Œ์„ ํฌํ•จํ•˜๋Š” Flask๋ฅผ ์‚ฌ์šฉํ•œ ์™„์ „ํ•œ JWT ์ธ์ฆ ์„œ๋น„์Šค ๊ตฌ์ถ•:

  1. ๋น„๋ฐ€๋ฒˆํ˜ธ ํ•ด์‹ฑ์ด ์žˆ๋Š” ์‚ฌ์šฉ์ž ๋“ฑ๋ก (argon2 ๋˜๋Š” bcrypt)
  2. ์•ก์„ธ์Šค + ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋กœ๊ทธ์ธ ์—”๋“œํฌ์ธํŠธ
  3. ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ์ˆœํ™˜์ด ์žˆ๋Š” ํ† ํฐ ๋ฆฌํ”„๋ ˆ์‹œ ์—”๋“œํฌ์ธํŠธ
  4. ํ† ํฐ ๋ธ”๋ž™๋ฆฌ์ŠคํŠธ๊ฐ€ ์žˆ๋Š” ๋กœ๊ทธ์•„์›ƒ ์—”๋“œํฌ์ธํŠธ (Redis ๋˜๋Š” ์ธ๋ฉ”๋ชจ๋ฆฌ ์„ธํŠธ ์‚ฌ์šฉ)
  5. "admin" ๋ฒ”์œ„๊ฐ€ ํ•„์š”ํ•œ ๋ณดํ˜ธ๋œ ์—”๋“œํฌ์ธํŠธ
  6. ๋งŒ๋ฃŒ, ์ž˜๋ชป๋œ ํ˜•์‹ ๋ฐ ํ๊ธฐ๋œ ํ† ํฐ์— ๋Œ€ํ•œ ์ ์ ˆํ•œ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ
  7. ๋กœ๊ทธ์ธ ์—”๋“œํฌ์ธํŠธ์— ๋Œ€ํ•œ ์†๋„ ์ œํ•œ (IP๋‹น ๋ถ„๋‹น 5ํšŒ ์‹œ๋„)

์—ฐ์Šต 2: CORS ๋ณด์•ˆ ๊ฐ์‚ฌ

๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•˜๋Š” Python ์Šคํฌ๋ฆฝํŠธ ์ž‘์„ฑ:

  1. API URL ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ
  2. ๋‹ค์–‘ํ•œ Origin ํ—ค๋”๋กœ ์š”์ฒญ ์ „์†ก
  3. API๊ฐ€ ์ž„์˜์˜ ์ถœ์ฒ˜๋ฅผ ๋ฐ˜์˜ํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ (์ทจ์•ฝ์ )
  4. ์ž๊ฒฉ ์ฆ๋ช…์ด ์™€์ผ๋“œ์นด๋“œ ์ถœ์ฒ˜์™€ ํ•จ๊ป˜ ํ—ˆ์šฉ๋˜๋Š”์ง€ ํ™•์ธ
  5. ์ผ๋ฐ˜์ ์ธ ๋ฉ”์„œ๋“œ ๋ฐ ํ—ค๋”์— ๋Œ€ํ•œ ํ”„๋ฆฌํ”Œ๋ผ์ดํŠธ ์ฒ˜๋ฆฌ ํ…Œ์ŠคํŠธ
  6. ๊ฐ ์—”๋“œํฌ์ธํŠธ์— ๋Œ€ํ•œ ๋ณด์•ˆ ๋ณด๊ณ ์„œ ์ƒ์„ฑ

์—ฐ์Šต 3: GraphQL ๋ณด์•ˆ ๋ฏธ๋“ค์›จ์–ด

๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•˜๋Š” GraphQL ๋ณด์•ˆ ๋ฏธ๋“ค์›จ์–ด ๊ตฌํ˜„:

  1. ๊ตฌ์„ฑ ๊ฐ€๋Šฅํ•œ ์ตœ๋Œ€๊ฐ’์œผ๋กœ ์ฟผ๋ฆฌ ๊นŠ์ด ์ œํ•œ (๊ธฐ๋ณธ๊ฐ’: 10)
  2. ์ฟผ๋ฆฌ ๋น„์šฉ ๊ณ„์‚ฐ ๋ฐ ๋น„์šฉ์ด ๋งŽ์ด ๋“œ๋Š” ์ฟผ๋ฆฌ ๊ฑฐ๋ถ€
  3. ํ”„๋กœ๋•์…˜์—์„œ ์ธํŠธ๋กœ์ŠคํŽ™์…˜ ๋น„ํ™œ์„ฑํ™”
  4. ๋ฐฐ์น˜ ์ฟผ๋ฆฌ ํฌ๊ธฐ ์ œํ•œ
  5. ๋น„์šฉ ๋ฐ ์‹คํ–‰ ์‹œ๊ฐ„๊ณผ ํ•จ๊ป˜ ๋ชจ๋“  ์ฟผ๋ฆฌ ๋กœ๊ทธ
  6. ํ•ด์‹œ ํ—ˆ์šฉ ๋ชฉ๋ก์ด ์žˆ๋Š” ์ง€์† ์ฟผ๋ฆฌ ๊ตฌํ˜„

์—ฐ์Šต 4: ์Šฌ๋ผ์ด๋”ฉ ์œˆ๋„์šฐ๊ฐ€ ์žˆ๋Š” ์†๋„ ์ œํ•œ๊ธฐ

๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•˜๋Š” ์Šฌ๋ผ์ด๋”ฉ ์œˆ๋„์šฐ ๋กœ๊ทธ ์†๋„ ์ œํ•œ๊ธฐ ๊ตฌํ˜„:

  1. Redis๋ฅผ ๋ฐฑ์—… ์ €์žฅ์†Œ๋กœ ์‚ฌ์šฉ (๋˜๋Š” ์ธ๋ฉ”๋ชจ๋ฆฌ ์‹œ๋ฎฌ๋ ˆ์ด์…˜)
  2. ๊ฐ ์š”์ฒญ์˜ ์ •ํ™•ํ•œ ํƒ€์ž„์Šคํƒฌํ”„ ์ถ”์ 
  3. ๊ตฌ์„ฑ ๊ฐ€๋Šฅํ•œ ์œˆ๋„์šฐ ์ง€์› (์ดˆ๋‹น, ๋ถ„๋‹น, ์‹œ๊ฐ„๋‹น)
  4. ๋‹ค๋ฅธ API ํ‚ค ํ‹ฐ์–ด์— ๋Œ€ํ•œ ๋‹ค๋ฅธ ์ œํ•œ ์ง€์›
  5. ์ ์ ˆํ•œ ์†๋„ ์ œํ•œ ํ—ค๋” ๋ฐ˜ํ™˜ (X-RateLimit-Limit, Remaining, Reset)
  6. ๋ถ„์‚ฐ ๋ฐฐํฌ ์ฒ˜๋ฆฌ (์—ฌ๋Ÿฌ ์„œ๋ฒ„ ์ธ์Šคํ„ด์Šค)

์—ฐ์Šต 5: API ์ž…๋ ฅ ๊ฒ€์ฆ ํ”„๋ ˆ์ž„์›Œํฌ

๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•˜๋Š” ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๊ฒ€์ฆ ํ”„๋ ˆ์ž„์›Œํฌ ๊ตฌ์ถ•:

  1. ์Šคํ‚ค๋งˆ์— ๋Œ€ํ•œ JSON ์š”์ฒญ ๋ณธ๋ฌธ ๊ฒ€์ฆ
  2. ํƒ€์ž… ๊ฐ•์ œ๋ฅผ ์‚ฌ์šฉํ•œ ์ฟผ๋ฆฌ ๋งค๊ฐœ๋ณ€์ˆ˜ ๊ฒ€์ฆ
  3. ๊ฒฝ๋กœ ๋งค๊ฐœ๋ณ€์ˆ˜ ๊ฒ€์ฆ
  4. ์ค‘์ฒฉ ๊ฐ์ฒด ๊ฒ€์ฆ ์ง€์›
  5. ์ผ๊ด€๋œ ์˜ค๋ฅ˜ ์‘๋‹ต ํ˜•์‹ ๋ฐ˜ํ™˜ (RFC 7807)
  6. ๋Œ€๋Ÿ‰ ํ• ๋‹น์œผ๋กœ๋ถ€ํ„ฐ ๋ณดํ˜ธ (์•Œ ์ˆ˜ ์—†๋Š” ํ•„๋“œ ๊ฑฐ๋ถ€)
  7. ๋ฌธ์ž์—ด ์ž…๋ ฅ ์ •์ œ (๊ณต๋ฐฑ ์ œ๊ฑฐ, ์œ ๋‹ˆ์ฝ”๋“œ ์ •๊ทœํ™”)

์—ฐ์Šต 6: API ๋ณด์•ˆ ์Šค์บ๋„ˆ

๋‹ค์Œ์— ๋Œ€ํ•œ API๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋Š” ๋ณด์•ˆ ์Šค์บ๋‹ ๋„๊ตฌ ์ƒ์„ฑ:

  1. ์—”๋“œํฌ์ธํŠธ์—์„œ ์ธ์ฆ ๋ˆ„๋ฝ
  2. ๊นจ์ง„ ๊ฐ์ฒด ์ˆ˜์ค€ ์ธ๊ฐ€ (BOLA/IDOR)
  3. ์†๋„ ์ œํ•œ ๋ˆ„๋ฝ
  4. ๋‚ด๋ถ€ ์„ธ๋ถ€ ์ •๋ณด๋ฅผ ๋“œ๋Ÿฌ๋‚ด๋Š” ์ž์„ธํ•œ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€
  5. ๋ณด์•ˆ ํ—ค๋” ๋ˆ„๋ฝ
  6. CORS ์ž˜๋ชป๋œ ๊ตฌ์„ฑ
  7. ์‹ฌ๊ฐ๋„ ๋“ฑ๊ธ‰์ด ์žˆ๋Š” ๋ณด๊ณ ์„œ ์ƒ์„ฑ

์š”์•ฝ

API ๋ณด์•ˆ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

๋ฒ”์ฃผ ํ•ญ๋ชฉ ์šฐ์„ ์ˆœ์œ„
์ธ์ฆ ์‚ฌ์šฉ์ž์—๊ฒŒ OAuth 2.0 ๋˜๋Š” JWT ์‚ฌ์šฉ (API ํ‚ค๋งŒ ์‚ฌ์šฉํ•˜์ง€ ๋ง ๊ฒƒ) Critical
์ธ์ฆ ๋‹จ๊ธฐ ์•ก์„ธ์Šค ํ† ํฐ (15๋ถ„) Critical
์ธ์ฆ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ์ˆœํ™˜ High
์ธ๊ฐ€ ๊ฐ์ฒด ์ˆ˜์ค€ ๊ถŒํ•œ ํ™•์ธ (BOLA ๋ฐฉ์ง€) Critical
์ธ๊ฐ€ ๋ชจ๋“  ์š”์ฒญ์—์„œ ๋ฒ”์œ„ ๊ฒ€์ฆ Critical
์†๋„ ์ œํ•œ IP๋‹น ๋ฐ ์‚ฌ์šฉ์ž๋‹น ์†๋„ ์ œํ•œ High
์†๋„ ์ œํ•œ ์ธ์ฆ ์—”๋“œํฌ์ธํŠธ์— ๋Œ€ํ•œ ์—„๊ฒฉํ•œ ์ œํ•œ High
์ž…๋ ฅ ๊ฒ€์ฆ ๋ชจ๋“  ์ž…๋ ฅ์— ๋Œ€ํ•œ ์Šคํ‚ค๋งˆ ๊ฒ€์ฆ Critical
์ž…๋ ฅ ๊ฒ€์ฆ ์•Œ ์ˆ˜ ์—†๋Š” ํ•„๋“œ ๊ฑฐ๋ถ€ High
CORS ํŠน์ • ์ถœ์ฒ˜ ํ—ˆ์šฉ ๋ชฉ๋ก (์ž๊ฒฉ ์ฆ๋ช…๊ณผ ํ•จ๊ป˜ ์™€์ผ๋“œ์นด๋“œ ์—†์Œ) Critical
CORS Vary: Origin ํ—ค๋” Medium
์•”ํ˜ธํ™” ์–ด๋””์„œ๋‚˜ TLS (HTTPS๋งŒ) Critical
๋กœ๊น… ๊ณ ์œ ํ•œ ์š”์ฒญ ID๋กœ ๋ชจ๋“  ์š”์ฒญ ๋กœ๊ทธ High
๋ฒ„์ „ ๊ด€๋ฆฌ ์ผ๋ชฐ ๋‚ ์งœ๋กœ ๊ตฌ๋ฒ„์ „ ํ๊ธฐ Medium
๊ฒŒ์ดํŠธ์›จ์ด ์ค‘์•™ ์ง‘์ค‘์‹ ์ธ์ฆ ๋ฐ ์†๋„ ์ œํ•œ Recommended

ํ•ต์‹ฌ ์š”์ 

  1. ์‹ฌ์ธต ๋ฐฉ์–ด โ€” ๋ชจ๋“  ๊ณ„์ธต(๋„คํŠธ์›Œํฌ, ๊ฒŒ์ดํŠธ์›จ์ด, ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค)์—์„œ ๋ณด์•ˆ ์ ์šฉ
  2. ํด๋ผ์ด์–ธํŠธ ์ž…๋ ฅ์„ ์ ˆ๋Œ€ ์‹ ๋ขฐํ•˜์ง€ ๋ง ๊ฒƒ โ€” ์„œ๋ฒ„์—์„œ ๋ชจ๋“  ๊ฒƒ์„ ๋งค๋ฒˆ ๊ฒ€์ฆ
  3. ํ™•๋ฆฝ๋œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ โ€” ์ž์ฒด ์ธ์ฆ ๋˜๋Š” ์•”ํ˜ธํ™”๋ฅผ ๋งŒ๋“ค์ง€ ๋ง ๊ฒƒ
  4. ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๊ฒฝ๊ณ  โ€” ๋ชจ๋‹ˆํ„ฐ๋ง ์—†๋Š” ๋ณด์•ˆ์€ ๋ถˆ์™„์ „ํ•จ
  5. API ๋ณด์•ˆ ๋ฌธ์„œํ™” โ€” ๋ช…ํ™•์„ฑ์„ ์œ„ํ•ด OpenAPI ๋ณด์•ˆ ์Šคํ‚ด ์‚ฌ์šฉ

์ด์ „: 09_Web_Security_Headers.md | ๋‹ค์Œ: 11_Secrets_Management.md

to navigate between lessons