OPEN HYPER STEP
← 목록으로 (stack-analysis)
STACK-ANALYSIS · 50 / 90
stack-analysis
CHAPTER 50 / 90
읽기 약 2
FUNCTION

API 보안: OAuth2 + API Key + Rate Limit


핵심 개념

OAuth2 4가지 그랜트·API Key 설계·Rate Limit·HMAC 서명 — 공개 API 보안.

본문

OAuth2 그랜트 타입

📋 코드 (15줄)
1. Authorization Code (가장 안전)
   - 웹·앱 사용자 로그인
   - PKCE 추가 (모바일·SPA)

2. Client Credentials
   - 서버 간 통신
   - 사용자 컨텍스트 없음

3. Resource Owner Password (구식·금지)
   - 사용자 비밀번호 직접 전달
   - 신뢰 관계 있을 때만

4. Refresh Token
   - 액세스 토큰 갱신
   - 별도 그랜트

Authorization Code + PKCE

📋 코드 (16줄)
[1] 클라이언트 → /authorize
   ?client_id=...
   &redirect_uri=...
   &response_type=code
   &scope=read:user
   &state=random
   &code_challenge=hash(verifier)
   &code_challenge_method=S256

[2] 사용자 로그인 + 동의

[3] 리다이렉트 → /callback?code=AUTH_CODE&state=...

[4] 클라이언트 → /token
   client_id, code, code_verifier
   → access_token, refresh_token

구현 — 인증 서버

TYPESCRIPT📋 코드 (37줄)
import crypto from 'crypto';

// 1. /authorize
app.get('/authorize', requireLogin, (req, res) => {
  const { client_id, redirect_uri, code_challenge, state } = req.query;
  const code = crypto.randomBytes(32).toString('hex');

  redis.setex(`auth_code:${code}`, 600, JSON.stringify({
    userId: req.user!.id,
    clientId: client_id,
    redirectUri: redirect_uri,
    codeChallenge: code_challenge,
  }));

  res.redirect(`${redirect_uri}?code=${code}&state=${state}`);
});

// 2. /token
app.post('/token', async (req, res) => {
  const { code, code_verifier, client_id } = req.body;

  const stored = await redis.get(`auth_code:${code}`);
  if (!stored) return res.status(400).json({ error: 'invalid_grant' });
  await redis.del(`auth_code:${code}`);

  const data = JSON.parse(stored);

  // PKCE 검증
  const challenge = crypto.createHash('sha256')
    .update(code_verifier).digest('base64url');
  if (challenge !== data.codeChallenge) {
    return res.status(400).json({ error: 'invalid_code_verifier' });
  }

  const tokens = issueTokens(data.userId, client_id);
  res.json(tokens);
});

API Key 설계

TYPESCRIPT📋 코드 (46줄)
// API Key 생성
async function createApiKey(userId: string, name: string, scopes: string[]) {
  const rawKey = crypto.randomBytes(32).toString('base64url');
  const prefix = rawKey.slice(0, 8);  // 표시용
  const hash = crypto.createHash('sha256').update(rawKey).digest('hex');

  await db.apiKey.create({
    data: {
      userId,
      name,
      prefix,
      hash,
      scopes,
      lastUsedAt: null,
      expiresAt: new Date(Date.now() + 365 * 86400 * 1000),
    },
  });

  // 평문 키는 1번만 표시
  return `sk_live_${rawKey}`;
}


// 검증 미들웨어
async function apiKeyAuth(req: Request, res: Response, next: NextFunction) {
  const auth = req.headers.authorization;
  if (!auth?.startsWith('Bearer sk_')) {
    return res.status(401).json({ error: 'invalid_api_key' });
  }
  const rawKey = auth.slice(11);
  const hash = crypto.createHash('sha256').update(rawKey).digest('hex');

  const apiKey = await db.apiKey.findUnique({ where: { hash } });
  if (!apiKey || apiKey.revokedAt || apiKey.expiresAt < new Date()) {
    return res.status(401).json({ error: 'invalid_api_key' });
  }

  // 비동기 — 사용 시각 기록
  db.apiKey.update({
    where: { id: apiKey.id },
    data: { lastUsedAt: new Date() },
  }).catch(console.error);

  req.apiKey = apiKey;
  next();
}

HMAC 서명 (웹훅)

TYPESCRIPT📋 코드 (37줄)
// 발신자
const signature = crypto.createHmac('sha256', WEBHOOK_SECRET)
  .update(JSON.stringify(payload))
  .digest('hex');

await fetch(targetUrl, {
  method: 'POST',
  headers: {
    'X-Signature': signature,
    'X-Timestamp': Date.now().toString(),
  },
  body: JSON.stringify(payload),
});


// 수신자 검증
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-signature'] as string;
  const timestamp = parseInt(req.headers['x-timestamp'] as string);

  // Replay 공격 방지
  if (Math.abs(Date.now() - timestamp) > 5 * 60 * 1000) {
    return res.status(401).json({ error: 'timestamp_too_old' });
  }

  const expected = crypto.createHmac('sha256', WEBHOOK_SECRET)
    .update(req.body)
    .digest('hex');

  if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
    return res.status(401).json({ error: 'invalid_signature' });
  }

  const payload = JSON.parse(req.body.toString());
  // ... 처리
  res.json({ received: true });
});

Scope 기반 권한

TYPESCRIPT📋 코드 (18줄)
function requireScope(...scopes: string[]) {
  return (req: Request, res: Response, next: NextFunction) => {
    const granted = req.apiKey?.scopes ?? req.user?.scopes ?? [];
    const hasAll = scopes.every(s => granted.includes(s));
    if (!hasAll) {
      return res.status(403).json({
        error: 'insufficient_scope',
        required: scopes,
      });
    }
    next();
  };
}


// 사용
app.get('/users', apiKeyAuth, requireScope('read:users'), listUsers);
app.delete('/users/:id', apiKeyAuth, requireScope('write:users', 'admin'), deleteUser);

다음 챕터

CH.51 "OpenAPI/Swagger 자동 문서화".


AI 프롬프트
🤖 AI에게 잘 물어보는 법 — 모델·전략별 프롬프트
Claude

무료: Sonnet 4.6 / Pro $20/mo: Opus 4.6

내 코드의 API 보안 부분을 분석해서
실전 분석 + 개선 우선순위를 알려줘.
ChatGPT

무료: GPT-5.5 / Plus $20/mo: GPT-5.5 Pro

API 보안 관련 인기 라이브러리/패턴 5개를
비교 분석해서 패턴 추출를 알려줘.
Gemini

무료: 2.5 Flash / Pro $19.99/mo: 3.1 Pro

내 프로젝트 전체에서 API 보안
최적화 가능 위치를 보고해줘.
Grok

무료: Grok 4.1 / SuperGrok $30/mo

2026년 한국 백엔드 시장의
API 보안 트렌드를 솔직히 알려줘.

⭐ 이것만 기억하세요
API 보안: OAuth2 + API Key + Rate Limit 이 3가지만 확실히 잡으세요
1.OAuth2 Authorization Code + PKCE = 모바일·SPA 표준
2.API Key는 hash 저장 + prefix만 표시 — 노출 시 즉시 revoke
3.HMAC 서명 + 타임스탬프 = 웹훅 보안 표준


공유하기
진행도 50 / 90