본문 바로가기
나의 IT 기억

🔐 SSO(Single Sign-On)란? 하나의 로그인으로 모든 서비스 이용하기

by 浪畅 (Làng Chàng) 2025. 5. 30.

🔐 SSO 시스템의 서버 간 API 키 인증 완벽 가이드

https://github.com/onesound71/sso_server
"하나의 키로 모든 서비스에 접근하는 마법"
- 서버 간 통신에서 SSO API 키를 활용한 자동화된 인증 시스템을 구축해보세요!

 

GitHub - onesound71/sso_server

Contribute to onesound71/sso_server development by creating an account on GitHub.

github.com

 

🎯  왜 서버 간 API 키 인증이 필요할까?

마이크로서비스 아키텍처에서 여러 서버가 서로 통신해야 하는 상황은 매우 흔합니다. 예를 들어:

  • 주문 서비스결제 서비스에 결제 요청
  • 사용자 서비스알림 서비스에 메시지 전송 요청
  • API 게이트웨이인증 서버에 토큰 검증 요청

이때 매번 사용자 ID/비밀번호로 로그인할 수는 없습니다. 서버는 24시간 자동으로 동작해야 하고, 사람의 개입 없이 인증되어야 하죠.

바로 이런 상황에서 API 키 인증이 빛을 발합니다!

🏗️ 시스템 아키텍처 이해하기

전체 구조 한눈에 보기

graph TB
    A[👤 사용자] --> B[🌐 브라우저 로그인]
    C[🤖 클라이언트 앱] --> D[🔑 API 키 인증]
    E[📱 Hello 서버<br/>포트: 5001] --> F[🔐 SSO 서버<br/>포트: 5002]

    B --> E
    D --> E
    E --> G[서버 간 인증<br/>자동 키 교환]
    F --> G

    subgraph "인증 방식"
        B
        D
        G
    end

    subgraph "보호된 리소스"
        H[📊 대시보드]
        I[🛡️ 관리자 페이지]
        J[📈 API 엔드포인트]
    end

    E --> H
    E --> I
    E --> J

핵심 구성 요소

🔐 SSO 서버 (포트 5002)

  • 역할: 중앙 인증 관리소
  • 기능: API 키 생성, 검증, 세션 관리
  • 비유: 은행의 본점 (모든 인증 결정)

📱 Hello 서버 (포트 5001)

  • 역할: 보호된 애플리케이션
  • 기능: 실제 서비스 제공, 인증 요청 전달
  • 비유: 은행의 지점 (서비스 제공하되 본점에서 인증 확인)

🔑 API 키 생성: 마스터 키를 만드는 과정

1단계: 기본 개념 이해

API 키는 서버의 신분증과 같습니다. 사람이 주민등록증을 가지고 은행에서 신원을 증명하듯, 서버는 API 키로 자신의 정체성을 증명합니다.

API 키의 구조:

sso_key_hello_server_default_12345
│       │     │      │        │
│       │     │      │        └─ 고유 식별자
│       │     │      └─ 용도/환경
│       │     └─ 클라이언트명
│       └─ 서비스 타입
└─ 프로젝트 접두사

2단계: CLI 도구로 키 생성하기

기본 명령어 구조:

python api_key_manager.py [명령] [매개변수]

새로운 API 키 생성:

# 일반적인 앱용 키 생성
python api_key_manager.py generate my-mobile-app

# 출력 예시:
# ✅ API 키가 성공적으로 생성되었습니다!
#    클라이언트: my-mobile-app
#    API 키: sso_key_my_mobile_app_7a9f2e8c1b5d
#    생성 시간: 2025-05-30 14:32:18

특별한 용도별 키 생성:

# 관리자 도구용
python api_key_manager.py generate admin-dashboard

# 결제 서비스용
python api_key_manager.py generate payment-service

# 데이터 분석 도구용
python api_key_manager.py generate analytics-bot

3단계: 생성된 키 관리하기

등록된 모든 키 확인:

python api_key_manager.py list

# 출력 예시:
# 📋 등록된 API 키 목록:
# 
# 1. 클라이언트: hello_server_default
#    키: sso_key_hello_server_default_12345
#    상태: ✅ 활성
# 
# 2. 클라이언트: my-mobile-app
#    키: sso_key_my_mobile_app_7a9f2e8c1b5d
#    상태: ✅ 활성
# 
# 총 2개의 API 키가 등록되어 있습니다.

특정 키 테스트:

python api_key_manager.py test sso_key_my_mobile_app_7a9f2e8c1b5d

# 출력 예시:
# 🔍 API 키 테스트 중...
# 
# ✅ API 키 검증 성공!
# 📊 테스트 결과:
#    클라이언트: my-mobile-app
#    인증 상태: 유효
#    응답 시간: 23ms
#    서버 연결: 정상

🤝 서버 간 인증: 자동화된 신뢰 관계 구축

핵심 아이디어: 신뢰의 체인

서버 간 인증은 "친구의 친구는 친구" 개념과 비슷합니다:

  1. Hello 서버SSO 서버를 신뢰
  2. SSO 서버API 키 소유자를 인증
  3. 따라서 Hello 서버API 키 소유자를 신뢰

자동 인증 흐름 상세 분석

# Hello 서버의 자동 인증 과정 (app.py에서)
class HelloServer:
    def __init__(self):
        self.sso_url = "http://localhost:5002"
        self.server_key = "sso_key_hello_server_default_12345"
        self.session_token = None

    def authenticate_with_sso(self):
        """SSO 서버와 자동 인증"""
        try:
            response = requests.post(
                f"{self.sso_url}/api/authenticate",
                headers={"X-API-Key": self.server_key},
                json={"api_key": self.server_key}
            )

            if response.status_code == 200:
                data = response.json()
                self.session_token = data.get('session_token')
                print(f"✅ SSO 서버와 인증 성공: {self.session_token}")
                return True
            else:
                print(f"❌ SSO 인증 실패: {response.status_code}")
                return False

        except Exception as e:
            print(f"🚨 SSO 연결 오류: {e}")
            return False

실제 동작 과정 단계별 분석

1단계: Hello 서버 시작

cd hello-server
python app.py

# 콘솔 출력:
# 🚀 Hello 서버 시작 중...
# 🔗 SSO 서버 연결 시도: http://localhost:5002
# 🔑 서버 키로 인증 중: sso_key_hello_server_default_12345
# ✅ SSO 서버와 인증 성공: session_abc123xyz789
# 🌐 서버 실행 중: http://localhost:5001

2단계: 클라이언트 요청 처리

@app.route('/')
def portal():
    # 1. API 키 확인
    api_key = request.args.get('api_key') or request.headers.get('X-API-Key')

    if api_key:
        # 2. SSO 서버에 API 키 검증 요청
        if validate_api_key_with_sso(api_key):
            # 3. 검증 성공 시 포털 페이지 제공
            return render_template('portal.html', 
                                 auth_method='API Key',
                                 client_info=get_client_info(api_key))

    # 4. API 키 없으면 브라우저 세션 확인
    return check_browser_session()

3단계: SSO 서버에서의 검증

@app.route('/api/authenticate', methods=['POST'])
def authenticate_api_key():
    api_key = request.headers.get('X-API-Key')

    # 1. 키 유효성 검증
    if api_key in valid_api_keys:
        # 2. 세션 토큰 생성
        session_token = generate_session_token()

        # 3. 클라이언트 정보 반환
        return jsonify({
            'status': 'success',
            'session_token': session_token,
            'client_name': valid_api_keys[api_key],
            'expires_in': 3600
        })

    return jsonify({'status': 'error', 'message': 'Invalid API key'}), 401

💻 실전 사용 예시: 다양한 시나리오

시나리오 1: 모바일 앱에서 API 접근

앱 개발자 입장:

// React Native 앱에서
class ApiClient {
    constructor() {
        this.apiKey = 'sso_key_my_mobile_app_7a9f2e8c1b5d';
        this.baseUrl = 'http://your-server.com:5001';
    }

    async fetchUserData() {
        try {
            const response = await fetch(`${this.baseUrl}/api/user`, {
                headers: {
                    'X-API-Key': this.apiKey,
                    'Content-Type': 'application/json'
                }
            });

            if (response.ok) {
                return await response.json();
            } else {
                throw new Error('API 인증 실패');
            }
        } catch (error) {
            console.error('API 요청 실패:', error);
        }
    }
}

시나리오 2: 마이크로서비스 간 통신

주문 서비스 → 결제 서비스:

# order_service.py
class OrderService:
    def __init__(self):
        self.payment_api_key = "sso_key_payment_service_prod_xyz789"
        self.payment_url = "http://payment-service:5001"

    def process_payment(self, order_id, amount):
        headers = {
            'X-API-Key': self.payment_api_key,
            'Content-Type': 'application/json'
        }

        payload = {
            'order_id': order_id,
            'amount': amount,
            'currency': 'KRW'
        }

        response = requests.post(
            f"{self.payment_url}/api/charge",
            headers=headers,
            json=payload
        )

        return response.json()

시나리오 3: 자동화 스크립트

배치 작업용 스크립트:

#!/bin/bash
# backup_script.sh

API_KEY="sso_key_backup_bot_daily_456"
SERVER_URL="http://localhost:5001"

# 1. API 키로 인증하여 데이터 추출
curl -H "X-API-Key: $API_KEY" \
     "$SERVER_URL/api/export/daily-data" \
     -o "backup_$(date +%Y%m%d).json"

# 2. 백업 완료 알림
curl -H "X-API-Key: $API_KEY" \
     -X POST "$SERVER_URL/api/notifications" \
     -d '{"message": "일일 백업 완료", "level": "info"}'

echo "✅ 백업 작업 완료: backup_$(date +%Y%m%d).json"

🔒 보안 모범 사례

1. API 키 보안 관리

❌ 절대 하지 말아야 할 것들:

// 클라이언트 사이드 코드에 직접 노출 (위험!)
const API_KEY = "sso_key_secret_123";  // ❌

// Git에 키를 커밋 (위험!)
config.js: apiKey: "sso_key_real_production_key"  // ❌

// 로그에 키 출력 (위험!)
console.log(`Using API key: ${apiKey}`);  // ❌

✅ 올바른 방법들:

# 환경 변수 사용
export SSO_API_KEY="sso_key_my_app_prod_xyz789"

# .env 파일 활용 (Git 제외)
echo "SSO_API_KEY=sso_key_my_app_prod_xyz789" > .env
echo ".env" >> .gitignore

# 컨테이너 시크릿 사용
kubectl create secret generic sso-key \
  --from-literal=api-key="sso_key_k8s_app_secret"

2. 키 순환 및 만료 관리

# api_key_rotation.py - 키 자동 순환 예시
import schedule
import time
from datetime import datetime, timedelta

class ApiKeyRotator:
    def __init__(self):
        self.current_keys = {}

    def rotate_key(self, client_name):
        """키 순환 로직"""
        # 1. 새 키 생성
        new_key = self.generate_new_key(client_name)

        # 2. 기존 키와 병행 사용 (grace period)
        old_key = self.current_keys.get(client_name)

        # 3. 7일 후 기존 키 비활성화
        schedule_key_deactivation(old_key, days=7)

        # 4. 클라이언트에 새 키 알림
        notify_client_new_key(client_name, new_key)

        print(f"🔄 {client_name} 키 순환 완료: {new_key}")

# 매월 1일 자동 키 순환
schedule.every().month.do(lambda: ApiKeyRotator().rotate_key("mobile-app"))

3. 모니터링 및 로깅

# api_usage_monitor.py
class ApiUsageMonitor:
    def log_api_request(self, api_key, endpoint, ip_address):
        """API 사용 로깅"""
        log_entry = {
            'timestamp': datetime.now().isoformat(),
            'api_key_hash': hashlib.sha256(api_key.encode()).hexdigest()[:16],
            'endpoint': endpoint,
            'ip_address': ip_address,
            'client_name': self.get_client_name(api_key)
        }

        # 보안 로그 저장
        self.save_security_log(log_entry)

        # 의심스러운 활동 감지
        if self.detect_suspicious_activity(api_key, ip_address):
            self.alert_security_team(log_entry)

    def detect_suspicious_activity(self, api_key, ip_address):
        """비정상 활동 감지"""
        # 1분에 100회 이상 요청
        if self.get_request_count(api_key, minutes=1) > 100:
            return True

        # 새로운 IP에서의 접근
        if not self.is_known_ip(api_key, ip_address):
            return True

        return False

🛠️ 고급 설정 및 커스터마이징

커스텀 API 키 형식 정의

# custom_key_generator.py
import secrets
import string
from datetime import datetime

class CustomKeyGenerator:
    def __init__(self, prefix="mycompany"):
        self.prefix = prefix

    def generate_key(self, client_name, environment="prod"):
        """커스텀 형식의 API 키 생성"""
        # 타임스탬프 기반 식별자
        timestamp = datetime.now().strftime("%Y%m%d")

        # 랜덤 문자열 생성
        random_part = ''.join(secrets.choice(
            string.ascii_lowercase + string.digits
        ) for _ in range(12))

        # 체크섬 계산
        checksum = self.calculate_checksum(client_name, timestamp, random_part)

        # 최종 키 조합
        api_key = f"{self.prefix}_key_{environment}_{client_name}_{timestamp}_{random_part}_{checksum}"

        return api_key

    def calculate_checksum(self, *parts):
        """간단한 체크섬 계산"""
        combined = ''.join(str(part) for part in parts)
        return format(hash(combined) % 10000, '04d')

# 사용 예시
generator = CustomKeyGenerator("acme")
key = generator.generate_key("mobile-app", "staging")
# 결과: acme_key_staging_mobile_app_20250530_a7f9e2c8b1d5_7382

환경별 키 관리 시스템

# environment_manager.py
class EnvironmentKeyManager:
    def __init__(self):
        self.environments = {
            'development': {
                'prefix': 'dev',
                'expiry_days': 30,
                'rate_limit': 1000
            },
            'staging': {
                'prefix': 'stage',
                'expiry_days': 14,
                'rate_limit': 500
            },
            'production': {
                'prefix': 'prod',
                'expiry_days': 90,
                'rate_limit': 100
            }
        }

    def create_environment_key(self, client_name, environment):
        """환경별 키 생성"""
        if environment not in self.environments:
            raise ValueError(f"지원하지 않는 환경: {environment}")

        env_config = self.environments[environment]

        key = f"sso_key_{env_config['prefix']}_{client_name}_{self.generate_random()}"

        # 키 메타데이터 저장
        self.save_key_metadata(key, {
            'client_name': client_name,
            'environment': environment,
            'rate_limit': env_config['rate_limit'],
            'expires_in_days': env_config['expiry_days'],
            'created_at': datetime.now().isoformat()
        })

        return key

📊 성능 최적화 및 모니터링

인증 성능 최적화

# auth_cache.py
import redis
import json
from functools import wraps

class AuthCache:
    def __init__(self):
        self.redis_client = redis.Redis(host='localhost', port=6379, db=0)
        self.cache_ttl = 300  # 5분 캐시

    def cached_auth(self, func):
        """인증 결과 캐싱 데코레이터"""
        @wraps(func)
        def wrapper(api_key, *args, **kwargs):
            # 캐시에서 확인
            cache_key = f"auth:{hashlib.md5(api_key.encode()).hexdigest()}"
            cached_result = self.redis_client.get(cache_key)

            if cached_result:
                print(f"🚀 캐시 히트: {api_key[:20]}...")
                return json.loads(cached_result)

            # 캐시 미스 - 실제 인증 수행
            result = func(api_key, *args, **kwargs)

            # 결과 캐싱
            if result.get('valid'):
                self.redis_client.setex(
                    cache_key, 
                    self.cache_ttl, 
                    json.dumps(result)
                )
                print(f"💾 캐시 저장: {api_key[:20]}...")

            return result

        return wrapper

# 사용 예시
auth_cache = AuthCache()

@auth_cache.cached_auth
def validate_api_key(api_key):
    """API 키 검증 (캐싱 적용)"""
    # 실제 검증 로직
    return sso_server.authenticate(api_key)

실시간 메트릭 수집

# metrics_collector.py
from prometheus_client import Counter, Histogram, generate_latest
import time

# 메트릭 정의
auth_requests_total = Counter(
    'sso_auth_requests_total', 
    'Total authentication requests',
    ['method', 'status']
)

auth_duration_seconds = Histogram(
    'sso_auth_duration_seconds',
    'Time spent on authentication'
)

class MetricsCollector:
    @staticmethod
    def record_auth_request(method, status, duration):
        """인증 요청 메트릭 기록"""
        auth_requests_total.labels(method=method, status=status).inc()
        auth_duration_seconds.observe(duration)

    @staticmethod
    def auth_metrics_middleware(func):
        """인증 메트릭 수집 미들웨어"""
        @wraps(func)
        def wrapper(*args, **kwargs):
            start_time = time.time()

            try:
                result = func(*args, **kwargs)
                status = 'success' if result.get('valid') else 'failed'
            except Exception as e:
                status = 'error'
                raise
            finally:
                duration = time.time() - start_time
                method = 'api_key'  # 또는 요청에서 추출
                MetricsCollector.record_auth_request(method, status, duration)

            return result

        return wrapper

# 메트릭 엔드포인트
@app.route('/metrics')
def metrics():
    return generate_latest(), 200, {'Content-Type': 'text/plain'}

🎯 마무리: SSO의 힘을 활용하세요!

핵심 요약

🔑 API 키 생성의 핵심:

  • 명확한 명명 규칙: prefix_type_client_identifier
  • 환경별 관리: dev, staging, prod 분리
  • 정기적 순환: 보안을 위한 키 교체

🤝 서버 간 인증의 핵심:

  • 자동화된 신뢰: 한 번 설정하면 계속 동작
  • 토큰 기반 세션: 효율적인 연결 유지
  • 장애 복구: 연결 끊김 시 자동 재연결

다음 단계 제안

  1. 보안 강화: JWT 토큰 도입, 키 암호화
  2. 확장성 개선: 로드 밸런서, 분산 캐시
  3. 모니터링 고도화: 실시간 대시보드, 알림 시스템

마지막 팁: 시작은 간단하게, 발전은 단계적으로! 기본 API 키 시스템부터 구축한 후 필요에 따라 고급 기능을 추가해나가세요.