Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
215 changes: 215 additions & 0 deletions keyword/Chapter04/keyword.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
# 인증(Authentication)과 인가(Authorization)

---

## 0. 개요

* 인증(Authentication): “너는 누구냐?” → 사용자의 신원을 확인하는 과정
* 인가(Authorization): “너는 무엇을 할 수 있냐?” → 인증된 사용자의 권한을 확인하는 과정
* 순서: **인증 → 인가**
* 실패 시 상태 코드

* 인증 실패 → `401 Unauthorized`
* 인가 실패 → `403 Forbidden`

---

## 1. 인증(Authentication)

### 1-1. 정의

* 사용자가 주장하는 **신원(Identity)** 이 실제로 맞는지를 확인하는 절차

### 1-2. 주요 방법

* ID + Password
* OAuth (소셜 로그인)
* 2FA/MFA (OTP, 문자 인증 등)
* 생체인증 (지문, 얼굴 인식 등)

### 1-3. 인증 성공 후 동작

* 서버는 인증 성공 시 **세션(Session)** 또는 **JWT 토큰**을 발급
* 이 토큰은 이후 요청 시 “내가 인증된 사용자임”을 증명

---

## 2. 인가(Authorization)

### 2-1. 정의

* 인증된 사용자가 **어떤 자원(Resource)에 접근 가능한지** 확인하는 절차

### 2-2. 주요 방식

* **RBAC (Role-Based Access Control)** → 역할 기반 (admin, user 등)
* **ABAC (Attribute-Based Access Control)** → 속성 기반 (등급, 구독 상태 등)
* **정책 기반 제어** → 시간, IP, 네트워크 조건에 따른 접근 제한

### 2-3. 예시

* 로그인은 했지만 관리자 권한이 없어 `/admin` 접근 시 `403 Forbidden`

---

## 3. 인증 → 인가 전체 흐름

```mermaid
graph TD
A["사용자 요청 (로그인 시도)"] --> B{"인증(Authentication)"}
B -->|실패| E["401 Unauthorized"]
B -->|성공| C{"인가(Authorization)"}
C -->|권한 없음| F["403 Forbidden"]
C -->|권한 있음| D["정상 접근"]
```

---

## 4. 세션(Session)과 JWT 비교

### 4-1. 세션(Session)

* 서버가 상태(State)를 직접 저장
* 로그인 성공 시 **세션 ID** 발급 → 쿠키에 저장

#### 4-1-1. 장점

* 서버에서 즉시 무효화 가능
* 브라우저가 쿠키 자동 전송
* 관리 직관적, 보안성 높음

#### 4-1-2. 단점

* 서버 확장 시 세션 동기화 필요
* CSRF 공격에 취약 (자동 전송 특성)
* Redis 같은 중앙 세션 저장소 필요

#### 4-1-3. 쿠키 보안 옵션

| 옵션 | 설명 |
| -------- | ----------------- |
| HttpOnly | JS 접근 차단 (XSS 방지) |
| Secure | HTTPS에서만 전송 |
| SameSite | 크로스 도메인 제어 |

---

### 4-2. JWT (JSON Web Token)

* **무상태(stateless)** 인증 — 서버가 사용자 상태를 저장하지 않음
* 클라이언트가 `Authorization: Bearer <token>` 형태로 토큰 전송

#### 4-2-1. 장점

* 서버 확장성 높음 (스케일 아웃 구조에 유리)
* 마이크로서비스 간 인증 공유 용이
* 클라이언트 친화적 (웹/모바일 공통 사용 가능)

#### 4-2-2. 단점

* 탈취 시 위험 (즉시 무효화 어려움)
* 보안 설계 복잡 (토큰 저장, 만료, 회전 등)

#### 4-2-3. 보안 전략

* Access Token 짧게 (15~30분)
* Refresh Token 길게 (7~30일)
* 블랙리스트 or 버전 관리로 무효화 대응

---

### 4-3. 세션 vs JWT 비교 요약

| 항목 | **Session** | **JWT** |
| ----- | -------------- | ---------------------- |
| 상태 관리 | 서버 저장 | 무상태 (클라이언트 보관) |
| 저장 위치 | 서버 (메모리/Redis) | 클라이언트 (Storage/Cookie) |
| 만료 제어 | 즉시 가능 | 불가능 (회전 필요) |
| 확장성 | 낮음 | 높음 |
| 주요 공격 | CSRF | XSS |

---

## 5. 토큰(Token) 개념

### 5-1. 정의

* 인증/인가를 위해 발급되는 **문자열 형태의 인증 증명 수단**
* 일종의 “출입증”

### 5-2. 종류

| 종류 | 설명 |
| ------------ | ------------------------------------ |
| Basic Token | `Base64(ID:PW)` 인코딩 (보안 낮음) |
| Bearer Token | `Authorization: Bearer <token>` (표준) |

---

## 6. Access Token vs Refresh Token

| 구분 | **Access Token** | **Refresh Token** |
| ---- | ------------------ | ----------------- |
| 역할 | API 접근 인증/인가 | Access Token 재발급 |
| 수명 | 짧음 (15~30분) | 김 (7~30일) |
| 저장 | 메모리 / localStorage | HttpOnly Cookie |
| 탈취 시 | 위험도 높음 → 짧게 유지 | 재발급 제한 필요 |

### 6-1. 동작 흐름

1. 로그인 시 Access + Refresh Token 발급
2. Access Token으로 요청
3. 만료 시 Refresh Token으로 재발급
4. Refresh Token 만료 시 재로그인

---

## 7. 클라이언트 저장소 보안 비교

| 저장소 | 자동 전송 | JS 접근 가능 | 주요 보안 위험 | 권장 사용 |
| ------------------ | ----- | ----------------- | -------- | ------------- |
| **Cookie** | ✅ | ❌ (HttpOnly 설정 시) | CSRF | Refresh Token |
| **localStorage** | ❌ | ✅ | XSS | Access Token |
| **sessionStorage** | ❌ | ✅ | XSS | 탭 단위 임시 토큰 |

---

## 8. HTTP는 왜 무상태(Stateless)인가?

### 8-1. 이유

* 요청 간 상태를 저장하지 않음으로써

* **확장성(Scalability)** 확보
* **복원력(Fault Tolerance)** 강화
* **로드밸런싱 및 수평 확장** 용이

### 8-2. 단점

* 로그인, 장바구니 등 “상태 유지” 기능은 별도 설계 필요
→ 세션/토큰으로 상태 보완

---

## 9. 세션 분산(Scalable Session) 전략

| 방식 | 개념 | 장점 | 단점 |
| ------------------------- | ----------------- | -------- | ------------- |
| **Sticky Session** | 동일 사용자를 항상 같은 서버로 | 간단 | 서버 장애 시 세션 유실 |
| **Session Replication** | 서버 간 세션 복제 | 고가용성 | 네트워크 부하 |
| **Central Store (Redis)** | 중앙 세션 저장소 활용 | 확장성, 단순성 | 저장소 장애 리스크 |

---

## 10. 하이브리드 방식 (JWT + 저장소)

### 10-1. 개념

* JWT 발급 시 `jti(토큰 ID)`와 `tokenVersion`을 DB에 저장
* 요청 시 JWT 서명 + jti/version 검증
* 불일치 시 즉시 차단

### 10-2. 장점

* JWT의 무상태성과 세션의 제어성 결합
* MSA·분산 환경에서 실용적 균형
7 changes: 3 additions & 4 deletions mission/Chapter03/src/api/movie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,21 @@ interface GetMovieParams {
export async function getMovies({
category,
page = 1,
language = "ko-KR",
language = "ko-KR"
}: GetMoviesParams): Promise<MovieRes> {
const { data } = await api.get<MovieRes>(`/movie/${category}`, {
params: { language, page },
params: { language, page }
});
return data;
}

export async function getMovie({
movieId,
language = "ko-KR",
language = "ko-KR"
}: GetMovieParams): Promise<MovieDetails> {
const { data } = await api.get<MovieDetails>(`/movie/${movieId}`, {
params: { language },
});
console.log(data);
return data;
}

Expand Down
37 changes: 37 additions & 0 deletions mission/Chapter03/src/hooks/useCustomFetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useEffect, useRef, useState, type DependencyList } from 'react';

type FetchFactory<T> = (signal?: AbortSignal) => Promise<T>;

export function useCustomFetch<T>(factory: FetchFactory<T>, deps: DependencyList = []) {
const [data, setData] = useState<T | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const controllerRef = useRef<AbortController | null>(null);

const run = async () => {
setIsLoading(true);
setIsError(false);

controllerRef.current?.abort();
controllerRef.current = new AbortController();

try {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

controllerRef를 통해 기존 요청을 중단하고 새 요청으로 교체하는 구조가 아주 좋습니다.
다만 에러 발생 후 재요청 시 이전 에러 상태가 유지되지 않도록, setIsError(false)를 통해 에러 상태를 수동으로 초기화하는 것을 추천합니다

const result = await factory(controllerRef.current.signal);
setData(result);
} catch (e: any) {
if (e?.name === 'AbortError') {
} else {
setIsError(true);
}
} finally {
setIsLoading(false);
}
};

useEffect(() => {
run();
return () => controllerRef.current?.abort();
}, deps);

return { data, isLoading, isError, refetch: run };
}
69 changes: 27 additions & 42 deletions mission/Chapter03/src/pages/MovieDetailPage.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useEffect, type ReactElement, useState } from "react";
import { getCredits, getMovie } from "../api/movie";
import { type ReactElement } from "react";
import { useParams } from "react-router-dom";
import { getCredits, getMovie } from "../api/movie";
import type { Credits, MovieDetails } from "../types/Movie";
import LoadingSpinner from "../components/LoadingSpinner";
import { useCustomFetch } from "../hooks/useCustomFetch";

function formatYear(dateStr?: string): string {
if (!dateStr) return "";
Expand All @@ -23,48 +24,30 @@ export function buildImageUrl(
return `https://image.tmdb.org/t/p/${size}${path}`;
}

const MovieDetailPage = (): ReactElement => {
const [isPending, setIsPending] = useState<boolean>(false);
const [isError, setIsError] = useState<boolean>(false);
const [detail, setDetail] = useState<MovieDetails | null>(null);
const [credits, setCredits] = useState<Credits | null>(null);
const [creditError, setCreditError] = useState<boolean>(false);

const { movieId } = useParams<{ movieId: string }>();
const safeMovieId = typeof movieId === "string" ? movieId : "1";

useEffect((): void => {
const run = async (): Promise<void> => {
setIsPending(true);
setIsError(false);
setCreditError(false);
setDetail(null);
setCredits(null);

async function fetchDetailAndCredits(
movieId: string,
): Promise<{ detail: MovieDetails; credits: Credits | null; creditError: boolean }> {
const [detailData, maybeCredits] = await Promise.all([
getMovie({ movieId, language: "ko-KR" }),
(async () => {
try {
const [detailData, maybeCredits] = await Promise.all([
getMovie({ movieId: safeMovieId, language: "ko-KR" }),
(async () => {
try {
return await getCredits({ movieId: safeMovieId, language: "ko-KR" });
} catch {
setCreditError(true);
return null;
}
})(),
]);

setDetail(detailData);
setCredits(maybeCredits);
return await getCredits({ movieId, language: "ko-KR" });
} catch {
setIsError(true);
} finally {
setIsPending(false);
return null;
}
};
})(),
]);
return { detail: detailData, credits: maybeCredits, creditError: !maybeCredits };
}

const MovieDetailPage = (): ReactElement => {
const { movieId } = useParams<{ movieId: string }>();
const safeMovieId = typeof movieId === "string" ? movieId : "1";

run();
}, [safeMovieId]);
const { data, isLoading, isError } = useCustomFetch(
() => fetchDetailAndCredits(safeMovieId),
[safeMovieId]
);

if (isError) {
return (
Expand All @@ -74,14 +57,16 @@ const MovieDetailPage = (): ReactElement => {
);
}

if (isPending || !detail) {
if (isLoading || !data?.detail) {
return (
<div className="flex item-center justify-center h-dvh">
<LoadingSpinner />
</div>
);
}

const { detail, credits, creditError } = data;

const posterUrl = buildImageUrl(detail.poster_path, "w500");
const year = formatYear(detail.release_date);
const runtimeText = formatRuntime(detail.runtime);
Expand Down Expand Up @@ -187,7 +172,7 @@ const MovieDetailPage = (): ReactElement => {
credit 정보를 불러올 수 없습니다.
</p>
) : (
<ul className="mt-2 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
<ul className="mt-2 grid grid-cols-1 sm:grid-cols-2 lg:gridCols-3 gap-3">
{castTop.map((c) => (
<li
key={`${c.credit_id}-${c.id}`}
Expand Down
Loading