stack-analysis
CHAPTER 50 / 90
읽기 약 2분
FUNCTION
API 보안: OAuth2 + API Key + Rate Limit
핵심 개념
OAuth2 4가지 그랜트·API Key 설계·Rate Limit·HMAC 서명 — 공개 API 보안.
본문
OAuth2 그랜트 타입
1. Authorization Code (가장 안전)
- 웹·앱 사용자 로그인
- PKCE 추가 (모바일·SPA)
2. Client Credentials
- 서버 간 통신
- 사용자 컨텍스트 없음
3. Resource Owner Password (구식·금지)
- 사용자 비밀번호 직접 전달
- 신뢰 관계 있을 때만
4. Refresh Token
- 액세스 토큰 갱신
- 별도 그랜트Authorization Code + PKCE
[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구현 — 인증 서버
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 설계
// 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 서명 (웹훅)
// 발신자
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 기반 권한
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