API Key Authentication

API Key Authentication은 마이크로서비스 아키텍처(MSA)에서 보안을 위해 사용되는 중요한 인증 방식이다.

API Key Authentication은 클라이언트가 API에 접근할 때 고유한 식별자(API 키)를 사용하여 인증하는 방식이다. 이 키는 서버에서 생성하여 클라이언트에게 제공되며, 클라이언트는 API 요청 시 이 키를 포함시켜 자신의 신원을 증명한다.

API Key Authentication은 구현이 간단하고 사용하기 쉽다는 장점이 있지만, 보안 측면에서는 제한적이다. 따라서 중요한 데이터나 높은 보안이 요구되는 서비스에는 OAuth2나 JWT와 같은 더 강력한 인증 방식을 고려해야 한다.

작동 방식

  1. 서버가 클라이언트에게 고유한 API 키를 발급한다.
  2. 클라이언트는 API 요청 시 이 키를 헤더, 쿼리 파라미터 또는 요청 본문에 포함시킨다.
    • HTTP 헤더: X-API-Key: your_api_key
    • 쿼리 파라미터: https://api.example.com/data?api_key=your_api_key
  3. 서버는 요청에 포함된 API 키를 확인하여 클라이언트를 인증한다.

구현 방법

  1. API 키 생성: 서버에서 무작위의 문자열로 API 키를 생성한다.
  2. 키 저장: 생성된 키를 안전한 데이터베이스에 저장한다.
  3. 키 전달: 안전한 채널을 통해 클라이언트에게 키를 전달한다.
  4. 요청 처리: API 요청 시 키를 확인하고 유효한 경우에만 요청을 처리한다.

장점

  1. 구현이 간단하다.
  2. 클라이언트 측에서 사용하기 쉽다.
  3. 서버 측에서 API 사용량을 모니터링하고 제어하기 용이하다.

단점

  1. API 키가 노출되면 보안에 취약해진다.
  2. 키가 탈취되면 즉시 대응하기 어렵다.
  3. 사용자별 권한 관리가 제한적이다.

보안 강화 방법

  1. HTTPS 사용: 모든 API 통신에 HTTPS를 적용하여 키 전송을 암호화한다.
  2. 키 순환: 정기적으로 API 키를 갱신한다.
  3. 접근 제한: IP 화이트리스팅 등을 통해 키 사용을 제한한다.

사용 사례

  1. 공개 API: 날씨 정보, 지도 서비스 등의 공개 API에서 사용량 제한을 위해 사용된다.
  2. B2B 서비스: 기업 간 데이터 교환 시 간단한 인증 방식으로 사용된다.
  3. 개발자 포털: API 사용량 추적 및 과금을 위해 사용된다.

구현 예시

레이트 리미팅과 권한 관리를 포함한 API Key 인증 시스템 구현

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
class EnhancedAPIKeyAuthService {
    constructor(redisClient, dbClient) {
        this.redis = redisClient;
        this.db = dbClient;
        this.rateLimitWindow = 3600; // 1시간
    }

    async validateRequest(apiKey, resource, method) {
        // 1. API 키 유효성 검증
        const keyData = await this.validateApiKey(apiKey);
        if (!keyData) {
            throw new UnauthorizedError('Invalid API key');
        }

        // 2. 레이트 리미팅 검사
        const isWithinLimit = await this.checkRateLimit(apiKey);
        if (!isWithinLimit) {
            throw new RateLimitExceededError('Rate limit exceeded');
        }

        // 3. 권한 검증
        const hasPermission = await this.checkPermission(
            keyData.permissions, 
            resource, 
            method
        );
        if (!hasPermission) {
            throw new ForbiddenError('Insufficient permissions');
        }

        // 4. 사용 기록
        await this.logApiUsage(apiKey, resource, method);

        return keyData;
    }

    async validateApiKey(apiKey) {
        // 캐시 확인
        const cached = await this.redis.get(`api_key:${apiKey}`);
        if (cached) {
            return JSON.parse(cached);
        }

        // DB에서 키 정보 조회
        const keyData = await this.db.apiKeys.findOne({
            key: apiKey,
            isActive: true
        });

        if (keyData) {
            // 캐시에 저장
            await this.redis.setex(
                `api_key:${apiKey}`,
                3600,
                JSON.stringify(keyData)
            );
        }

        return keyData;
    }

    async checkRateLimit(apiKey) {
        const key = `rate_limit:${apiKey}`;
        const current = await this.redis.incr(key);
        
        if (current === 1) {
            await this.redis.expire(key, this.rateLimitWindow);
        }
        
        return current <= this.getRateLimit(apiKey);
    }
}

참고 및 출처