// 이 파일이 하는 일: "인증 아키텍처 — 세션 vs JWT vs OAuth" 코스 — 로그인 뒤에 숨은
// 설계 결정을 7개 섹션으로 파고드는 고급 과정 정적 학습 페이지. 인증/인가 구분에서 출발해
// 세션·JWT·리프레시 토큰·저장 위치·OAuth2/OIDC·마이크로서비스 인증까지, "쿠키냐 토큰이냐"라는
// 질문을 '무상태의 자유와 대가'라는 저울로 다시 읽는다. 프레이밍은 "서비스를 만들어 본 사람"이
// 우리 실제 플랫폼(Spring 마이크로서비스 + JWT + Valkey 리프레시)의 설계 이유를 되짚는 것.
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 styles/global.css의
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용하고 색은 var(--*)만 쓴다(하드코딩 금지).
// 예제 코드는 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.

import { Link } from 'react-router-dom';

// ── 예제 코드 상수들 ──

const CODE_AUTHN_AUTHZ = `인증(Authentication)            인가(Authorization)
"너 누구야?"                    "너 이거 해도 돼?"

로그인 화면에서 일어나는 일       로그인 '이후' 매 요청마다 일어나는 일
아이디+비밀번호 → 신원 확인       역할(role)·소유권 → 권한 판단
실패하면 → 401 Unauthorized      실패하면 → 403 Forbidden

순서가 중요합니다: 반드시 인증이 먼저, 인가가 나중.
"누구인지도 모르는데 권한을 따질 수는 없다."

그리고 갈림길 하나 —
서버가 '누가 로그인해 있는지'를 기억하면  → 상태 유지(stateful, 세션)
서버가 아무것도 기억하지 않으면          → 무상태(stateless, JWT)
이 갈림길이 이 코스 전체를 관통하는 저울입니다.`;

const CODE_SESSION = `[세션 기반 인증의 한 사이클]

① 로그인   클라이언트 → 서버 : POST /login (id, pw)
② 발급     서버: 세션 저장소에 기록을 만든다
           sessionId=ab12cd → 사용자 7번, 로그인 시각, 역할 ...
③ 전달     서버 → 클라이언트 : Set-Cookie: SID=ab12cd; HttpOnly; Secure
④ 이후요청 클라이언트 → 서버 : Cookie: SID=ab12cd (브라우저가 자동 첨부)
⑤ 조회     서버: 저장소에서 ab12cd를 찾아 "아, 7번 사용자구나"
⑥ 로그아웃 서버: 저장소에서 ab12cd를 지운다 → 그 즉시 무효

핵심: 쿠키에 담긴 건 '열쇠 번호'뿐, 사용자 정보는 전부 서버에 있다.
그래서 서버가 마음만 먹으면 언제든 끊을 수 있다(강제 로그아웃이 쉽다).
대신 서버는 모든 로그인 사용자의 상태를 '기억'해야 한다(stateful).`;

const CODE_JWT = `[JWT 한 장의 해부 — 점(.)으로 나뉜 세 덩어리]

eyJhbGciOiJIUzI1NiJ9 . eyJzdWIiOiI3Iiwicm9sZSI6IlVTRVIifQ . dBjftJeZ4CVP...
────────────────────   ──────────────────────────────────   ──────────────
① 헤더(Header)          ② 페이로드(Payload)                  ③ 서명(Signature)
어떤 알고리즘으로         실제 내용(클레임). 예:                 헤더+페이로드를
서명했나 (HS256 등)       sub: "7"        (사용자 ID)           비밀키로 서명한 값.
                        role: "USER"                          한 글자라도 바뀌면
                        displayName: "김수습"                  서명이 안 맞는다.
                        expert: false
                        exp: 1767225600 (만료 시각)

검증: 서버가 받은 토큰의 ①+②를 자기 비밀키로 다시 서명해 ③과 비교.
일치하면 "내가 발급한 그대로다" — 저장소 조회 없이 그 자리에서 신뢰.

주의: ②는 암호화가 아니라 Base64 '인코딩'일 뿐!
누구나 디코딩해 읽을 수 있으니 비밀번호·주민번호는 절대 넣지 않는다.`;

const CODE_REFRESH = `[짧은 액세스 + 긴 리프레시 — 우리 auth 서비스의 흐름]

액세스 토큰   수명 ~15분. 매 API 요청에 실려 다닌다. 서버는 서명만 검증.
리프레시 토큰  수명 ~14일. 오직 "새 액세스 토큰 주세요"에만 쓴다.
              발급 즉시 Valkey에 저장 → 서버가 '기억하는' 토큰.

① 로그인          → 액세스 + 리프레시 발급, 리프레시는 Valkey에 기록
② 평소 요청        → 액세스 토큰만 검증 (Valkey 조회 없음, 무상태)
③ 액세스 만료(401) → 리프레시 토큰으로 POST /api/auth/refresh
④ 회전(rotation)   → Valkey 대조 후 새 액세스 + '새' 리프레시 발급,
                    옛 리프레시는 그 자리에서 폐기 (재사용되면 도난 신호!)
⑤ 로그아웃         → 서버가 Valkey에서 리프레시를 삭제
                    → 남은 액세스 토큰이 죽는 최대 15분만 버티면 완전 차단

설계의 저울: 무상태의 속도(②)는 지키고,
통제가 필요한 지점(④⑤)에만 최소한의 '상태'를 되살렸다.`;

const CODE_STORAGE = `[토큰을 어디에 두나 — 두 자리의 트레이드오프]

              localStorage                httpOnly 쿠키
─────────────────────────────────────────────────────────────
JS로 읽기      가능                        불가능 (스크립트에 안 보임)
XSS에 뚫리면   토큰이 통째로 유출           토큰 자체는 못 훔친다
CSRF          해당 없음(자동 전송 안 됨)    위험 있음 → SameSite로 방어
전송 방식      코드가 Authorization 헤더에   브라우저가 요청마다 자동 첨부
              직접 실어야 함
편의성        구현 단순, SPA에서 흔함       서버가 Set-Cookie로 관리

한 줄 정리:
localStorage는 CSRF엔 강하지만 XSS 한 방에 전부 내준다.
httpOnly 쿠키는 XSS로부터 토큰을 숨기고, CSRF는 SameSite=Lax/Strict +
CSRF 토큰으로 막는다. 현대 웹의 무게추는 httpOnly 쪽으로 기울어 있다.`;

const CODE_OAUTH = `[OAuth2 Authorization Code 흐름 — "구글로 로그인" 버튼 뒤에서]

등장인물: 사용자 / 우리 앱(클라이언트) / 구글(인가 서버 + 자원 서버)

① 사용자가 "구글로 로그인" 클릭
   → 우리 앱이 구글 인가 서버로 보낸다 (client_id, redirect_uri, scope 포함)
② 사용자가 '구글 화면에서' 로그인 + "이 앱에 프로필 제공 동의"
   → 비밀번호는 구글에만 입력된다. 우리 앱은 구경도 못 한다.
③ 구글 → 우리 앱 : redirect_uri로 '인가 코드' 전달 (1회용, 수명 짧음)
④ 우리 서버 → 구글 : 코드 + client_secret을 내밀고 토큰 교환
   → access_token (+ OIDC라면 id_token) 수령
⑤ 우리 서버: id_token 서명 검증 → "구글이 보증하는 이 사람" 확인
   → 우리 회원과 연결하고, '우리' 액세스/리프레시 토큰을 발급

왜 코드를 한 번 거치나? ③은 브라우저(앞문)로 오가서 노출 위험이 있고,
④는 서버끼리(뒷문)의 통신이라 client_secret으로 신원을 증명할 수 있다.
앞문으로는 1회용 코드만, 진짜 토큰은 뒷문으로 — 이게 이 흐름의 요지.`;

const CODE_MSA = `[마이크로서비스에서 "누가 검증하나" — 두 가지 배치]

(A) 게이트웨이 집중 검증                 (B) 서비스 자체 검증 ← 우리 방식
게이트웨이가 JWT를 까고                  게이트웨이는 라우팅만.
뒤 서비스엔 헤더로 전달                  auth·market·content... 각자가
                                       같은 비밀키로 서명을 직접 검증
장점: 검증 로직 한 곳                    장점: 게이트웨이가 뚫려도 각 서비스가
단점: 게이트웨이 뒤 내부망을                   최종 관문. 서비스 단독 테스트 쉬움
      '무조건 신뢰'하게 됨               단점: 검증 코드·키가 서비스마다 복제

서비스 사이의 신뢰는 별도 문제:
사용자 없이 서비스끼리 부르는 /internal/** 경로는
X-Internal-Key 라는 공유 비밀 헤더로 "동료 서비스 맞음"을 증명한다.
사용자의 신원이 필요하면? JWT를 그대로 전파(propagation)해서
받는 쪽도 다시 검증한다 — "전달받은 신뢰도 검증 후에 쓴다."`;

// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
function Section({ n, title, sub, children }) {
  return (
    <section className="step-card" id={`sec-${n}`}>
      <div className="step-head">
        <span className="step-num">{n}</span>
        <div>
          <h3>{title}</h3>
          {sub && <span className="step-sub">{sub}</span>}
        </div>
      </div>
      <div className="step-body">{children}</div>
    </section>
  );
}

function Code({ children }) {
  return <pre className="code-block"><code>{children}</code></pre>;
}

const SECTIONS = [
  { n: 1, label: '인증 vs 인가' },
  { n: 2, label: '세션 기반' },
  { n: 3, label: 'JWT 해부' },
  { n: 4, label: '액세스·리프레시' },
  { n: 5, label: '토큰 보관 위치' },
  { n: 6, label: 'OAuth2·OIDC' },
  { n: 7, label: 'MSA 인증' },
];

export default function Lv3AuthArchitecturePage() {
  return (
    <div>
      {/* 히어로: 이 코스가 어디서 출발해 어디까지 가는지 */}
      <div className="hero">
        <div className="eyebrow">Course · 고급 과정</div>
        <h1>🔑 인증 아키텍처<br />— 세션 vs JWT vs OAuth</h1>
        <p>
          로그인은 만들 줄 알아요. 이제 질문이 달라집니다 — <strong>왜 세션이 아니라
          JWT인가? 왜 리프레시 토큰을 Valkey에 두는가? 토큰은 어디에 보관해야 하는가?</strong>{' '}
          쿠키냐 토큰이냐의 선택은 취향이 아니라, <strong>무상태(stateless)의 자유와
          그 대가</strong>를 저울질하는 설계 결정이에요. 우리 플랫폼의 실제 auth 설계를
          해부하며 그 저울을 손에 넣어 봅시다.
        </p>
        <div className="chip-row">
          <span className="chip">예상 소요 65분</span>
          <span className="chip">실습: JWT+리프레시 시퀀스 다이어그램</span>
          <span className="chip">선수 지식: HTTP·쿠키·서비스 구축 경험</span>
        </div>
      </div>

      {/* 섹션 바로가기 */}
      <nav className="pill-nav">
        {SECTIONS.map((s) => (
          <a key={s.n} href={`#sec-${s.n}`}>
            <b>{s.n}</b>
            {s.label}
          </a>
        ))}
      </nav>

      <Section n={1} title="인증 vs 인가 — 문 앞의 두 가지 질문" sub="누구인가(401)와 되는가(403)는 다른 문제다">
        <p>
          인증 설계의 첫걸음은 용어를 가르는 거예요. <strong>인증(Authentication)</strong>은
          "너 누구야?" — 아이디와 비밀번호로 <strong>신원을 확인</strong>하는 일이고,{' '}
          <strong>인가(Authorization)</strong>는 "너 이거 해도 돼?" — 확인된 신원의{' '}
          <strong>권한을 판단</strong>하는 일입니다. 순서도 정해져 있어요. 누구인지 모르는데
          권한을 따질 수는 없으니, <strong>항상 인증이 먼저, 인가가 나중</strong>입니다.
        </p>
        <Code>{CODE_AUTHN_AUTHZ}</Code>
        <p>
          HTTP 상태 코드가 이 구분을 그대로 담고 있어요. <span className="icode">401
          Unauthorized</span>는 "당신이 누군지 모르겠다"(인증 실패), <span className="icode">403
          Forbidden</span>은 "누군지는 알겠는데 이건 안 된다"(인가 실패). 이 둘을 섞어 쓰면
          디버깅할 때 엉뚱한 곳을 파게 됩니다.
        </p>
        <p>
          그리고 이 코스 전체를 관통하는 갈림길이 하나 있어요. 인증에 성공한 뒤, 서버가
          "누가 로그인해 있는지"를 <strong>기억할 것인가(stateful)</strong>, 아니면{' '}
          <strong>기억하지 않을 것인가(stateless)</strong>. 기억하면 통제가 쉽고 확장이
          어려워지고, 기억하지 않으면 확장이 쉽고 통제가 어려워집니다. 섹션 2와 3이
          이 갈림길의 양쪽 끝이에요.
        </p>
        <div className="tip">
          <b>비유</b> 인증은 건물 로비의 <strong>신분증 확인</strong>, 인가는 각 층 문 앞의{' '}
          <strong>출입 카드 등급</strong>이에요. 신분증 확인을 통과했다고 모든 층에 들어갈 수
          있는 건 아니죠. 로비(인증)는 한 번, 층별 문(인가)은 매번 — 요청마다 반복됩니다.
        </div>
      </Section>

      <Section n={2} title="세션 기반 인증 — 서버가 기억하는 방식" sub="쿠키에는 열쇠 번호만, 진실은 서버에">
        <p>
          웹 인증의 원조는 <strong>세션</strong>이에요. 로그인에 성공하면 서버가 자기 쪽{' '}
          <strong>세션 저장소</strong>에 "세션 ab12cd = 7번 사용자"라는 기록을 만들고,
          클라이언트에겐 그 <strong>열쇠 번호(세션 ID)만</strong> 쿠키로 건넵니다. 이후 브라우저는
          요청마다 쿠키를 자동으로 실어 보내고, 서버는 번호로 저장소를 뒤져 "아, 7번이구나"를
          알아내죠.
        </p>
        <Code>{CODE_SESSION}</Code>
        <p>
          이 방식의 힘은 <strong>즉시 통제</strong>예요. 진실이 전부 서버에 있으니, 저장소에서
          기록 한 줄을 지우면 그 사용자는 <strong>그 순간</strong> 로그아웃됩니다. 계정 도용이
          의심될 때 강제로 끊어내는 것도 한 줄이면 되죠.
        </p>
        <p>
          대가는 <strong>확장할 때</strong> 드러납니다. 서버를 3대로 늘리면? 1번 서버 메모리에만
          있는 세션을 2번 서버는 모르니 로그인이 풀려요. 해법은 세션 저장소를{' '}
          <strong>Redis/Valkey 같은 공용 저장소로 빼는 것</strong> — 하지만 이제 모든 요청이
          "저장소 조회 1회"라는 통행료를 냅니다. 서버는 무거워지지 않지만, 저장소가 모두의
          병목이자 단일 장애점이 되죠.
        </p>
        <div className="warn">
          <b>세션의 고전적 공격 두 가지</b> <strong>세션 고정(fixation)</strong>: 공격자가 미리
          알아낸 세션 ID를 피해자에게 심어 두고, 피해자가 그 ID로 로그인하면 공격자도 같은
          세션을 공유하게 되는 수법 — 방어는 <strong>로그인 성공 시 세션 ID를 새로
          발급</strong>하는 것. <strong>세션 탈취(hijacking)</strong>: 쿠키를 훔쳐 그대로 행세하는
          수법 — 방어는 <span className="icode">HttpOnly</span>(JS로 못 읽게) +{' '}
          <span className="icode">Secure</span>(HTTPS로만 전송) + 짧은 만료입니다.
        </div>
      </Section>

      <Section n={3} title="JWT 제대로 알기 — 서명된 자기소개서" sub="무상태의 자유, '취소 불가'라는 대가">
        <p>
          <strong>JWT(JSON Web Token)</strong>는 발상을 뒤집어요. 서버가 기억하는 대신,{' '}
          <strong>사용자 정보를 토큰 안에 담아 서명해서</strong> 클라이언트에게 통째로 맡깁니다.
          점(.)으로 나뉜 세 덩어리 — <strong>헤더</strong>(어떤 알고리즘), <strong>페이로드</strong>
          (실제 내용), <strong>서명</strong>(위조 방지 도장)이에요.
        </p>
        <Code>{CODE_JWT}</Code>
        <p>
          검증의 핵심은 <strong>서명</strong>입니다. 서버는 받은 토큰의 헤더+페이로드를 자기
          비밀키로 다시 서명해 세 번째 덩어리와 비교해요. 일치하면 "내가 발급한 그대로, 한 글자도
          안 바뀌었다"가 수학적으로 보장됩니다. <strong>저장소 조회가 필요 없으니</strong> 서버를
          10대로 늘려도 같은 비밀키만 있으면 어느 서버든 즉시 검증할 수 있어요. 이게 무상태의
          자유입니다.
        </p>
        <p>
          우리 플랫폼의 토큰에는 <span className="icode">role</span>·<span className="icode">displayName</span>·
          <span className="icode">expert</span>가 담겨요. 덕분에 각 서비스는 DB를 안 보고도
          "관리자인가? 화면에 뭐라고 표시하나? 전문가 권한인가?"를 판단합니다. 단, 여기엔 두 가지
          주의가 따라요. 첫째, 페이로드는 <strong>암호화가 아니라 인코딩</strong> — 누구나 열어
          볼 수 있으니 민감정보는 금지. 둘째, 토큰에 담긴 값은 <strong>발급 시점의
          스냅샷</strong>이라, 관리자가 role을 바꿔도 기존 토큰이 만료될 때까지는 옛 role로
          행세합니다.
        </p>
        <div className="warn">
          <b>'취소 불가'라는 대가</b> 세션은 서버 기록을 지우면 즉시 끊기지만, JWT는 서버가
          기억하는 게 없으니 <strong>한번 발급한 토큰을 만료 전에 무효화할 방법이
          없습니다.</strong> 유출돼도 만료까지 유효하죠. 그래서 JWT 설계의 제1원칙은{' '}
          <strong>수명을 짧게</strong> — 그리고 짧은 수명이 만드는 "15분마다 재로그인?" 문제를
          다음 섹션의 리프레시 토큰이 풉니다.
        </div>
      </Section>

      <Section n={4} title="액세스·리프레시 토큰 — 무상태에 통제를 되살리다" sub="짧은 토큰 + 긴 토큰 + Valkey 회전, 우리 auth의 설계">
        <p>
          해법은 토큰을 <strong>두 장</strong>으로 나누는 거예요. <strong>액세스 토큰</strong>은
          수명 15분 안팎으로 짧게 — 유출돼도 피해 창이 좁습니다. <strong>리프레시 토큰</strong>은
          수명을 길게 주되, 오직 "새 액세스 토큰 주세요"라는 한 가지 용도로만 쓰고,{' '}
          <strong>발급 즉시 서버(Valkey)에 저장</strong>해요. 눈치채셨나요? 리프레시 토큰은
          사실상 <strong>세션의 귀환</strong>입니다 — 서버가 다시 '기억하는' 토큰이니까요.
        </p>
        <Code>{CODE_REFRESH}</Code>
        <p>
          우리 auth 서비스가 정확히 이 구조예요. 평소 API 요청(②)은 액세스 토큰 서명 검증만으로
          처리돼 <strong>Valkey를 건드리지 않고</strong>(무상태의 속도), 갱신(④)과 로그아웃(⑤)
          같은 <strong>통제가 필요한 순간에만</strong> Valkey를 봅니다. 무상태의 자유는 최대한
          누리되, 대가가 아픈 지점에만 최소한의 상태를 되살린 절충이죠.
        </p>
        <p>
          ④의 <strong>회전(rotation)</strong>은 그냥 재발급이 아니에요. 리프레시할 때마다{' '}
          <strong>새 리프레시 토큰을 주고 옛것은 즉시 폐기</strong>합니다. 이러면 공격자가
          리프레시 토큰을 훔쳐도 정상 사용자와 공격자 중 <strong>한쪽이 먼저 쓰는 순간 다른 쪽의
          토큰이 죽고</strong>, 이미 폐기된 토큰이 다시 들어오면 "도난당했구나"를 탐지해 해당
          사용자의 토큰을 전부 끊을 수 있어요.
        </p>
        <div className="tip">
          <b>서버측 로그아웃이 '진짜'인 이유</b> 클라이언트에서 토큰을 지우는 것만으로는
          로그아웃이 아니에요 — 복사해 둔 토큰은 여전히 살아 있으니까요. 우리 설계처럼{' '}
          <strong>서버가 Valkey에서 리프레시 토큰을 삭제</strong>해야 갱신 경로가 끊기고, 남은
          액세스 토큰의 잔여 수명(최대 15분)만 지나면 완전히 차단됩니다. 그 15분마저 못 참는
          서비스라면 액세스 토큰 <strong>블랙리스트</strong>를 두지만, 그만큼 매 요청 조회가
          늘어나 무상태의 이점을 깎아 먹죠 — 여기서도 저울질입니다.
        </div>
      </Section>

      <Section n={5} title="토큰을 어디에 보관하나" sub="localStorage vs httpOnly 쿠키 — 저장 위치가 곧 공격면">
        <p>
          토큰 설계가 끝나도 질문이 하나 남아요. 브라우저에서 <strong>그 토큰을 어디에
          두는가.</strong> 후보는 둘 — JS로 자유롭게 읽고 쓰는 <span className="icode">localStorage</span>,
          그리고 JS에게는 보이지 않는 <span className="icode">httpOnly</span> 쿠키. 저장 위치의
          선택이 곧 <strong>어떤 공격에 노출될지</strong>를 결정합니다.
        </p>
        <Code>{CODE_STORAGE}</Code>
        <p>
          <span className="icode">localStorage</span>의 약점은 <strong>XSS</strong>예요. 악성
          스크립트가 한 줄이라도 실행되면 <span className="icode">localStorage.getItem()</span>{' '}
          한 번에 토큰이 통째로 공격자 서버로 넘어갑니다. <span className="icode">httpOnly</span>{' '}
          쿠키는 스크립트에서 아예 읽을 수 없으니 XSS로도 <strong>토큰 자체는</strong> 못 훔쳐요.
          대신 브라우저가 요청마다 자동 첨부하는 성질 때문에 <strong>CSRF</strong>(다른 사이트가
          내 브라우저를 시켜 요청을 위조)에 노출되는데, 이건 <span className="icode">SameSite=Lax/Strict</span>{' '}
          속성과 CSRF 토큰으로 막는 정석이 있습니다. XSS·CSRF 공격의 원리는 8강(웹 보안)에서
          다뤘던 그대로예요 — 이 섹션은 그 지식이 <strong>저장소 선택</strong>으로 이어지는
          다리입니다.
        </p>
        <p>
          우리 플랫폼은 현재 토큰을 localStorage 계열에 보관하고 있고, 그래서{' '}
          <strong>httpOnly 쿠키 전환이 보안 과제로 잡혀 있어요.</strong> "동작하는데 왜
          바꾸나?"가 아니라 — XSS 한 방에 전 사용자의 토큰이 걸린 구조냐, 토큰만은 지켜지는
          구조냐의 차이입니다. 잘 만드는 법이란 이런 기본값을 다시 고르는 일이에요.
        </p>
        <div className="warn">
          <b>완벽한 자리는 없다</b> httpOnly로 옮겨도 XSS 자체가 무해해지는 건 아니에요 —
          스크립트가 토큰을 '못 읽을' 뿐, 사용자인 척 <strong>요청을 대신 보내는 것</strong>은
          여전히 가능합니다. 저장 위치는 피해 범위를 줄이는 장치지, XSS 방어(입력 이스케이프,
          CSP)를 대체하지 않아요. 방어는 언제나 겹겹이(defense in depth)입니다.
        </div>
      </Section>

      <Section n={6} title="OAuth2·OIDC — 비밀번호 없이 신뢰를 빌려오기" sub="'구글로 로그인' 버튼 뒤의 정확한 순서">
        <p>
          "구글로 로그인"의 본질은 <strong>비밀번호를 우리에게 알려주지 않고, 구글이 대신
          보증하게 하는 것</strong>이에요. 이를 위한 표준 절차가 <strong>OAuth2</strong>이고,
          그중 웹 서비스의 정석은 <strong>Authorization Code 흐름</strong>입니다. 한 단계씩
          따라가 봅시다.
        </p>
        <Code>{CODE_OAUTH}</Code>
        <p>
          흐름의 묘수는 <strong>인가 코드를 한 번 거치는 것</strong>이에요. 브라우저 리다이렉트
          (③)는 주소창·히스토리에 노출될 수 있는 '앞문'이라 <strong>1회용 코드만</strong> 태우고,
          진짜 토큰은 우리 서버와 구글 서버가 <span className="icode">client_secret</span>으로
          서로를 확인하는 '뒷문'(④)으로만 오갑니다. 노출 위험이 있는 길에는 가치가 낮은 것만
          싣는다 — 보안 설계의 반복되는 문법이죠.
        </p>
        <p>
          여기서 용어 하나를 정확히 가르고 갑시다. <strong>OAuth2는 인가(Authorization)
          프로토콜</strong>이에요 — "이 앱이 내 구글 캘린더를 읽어도 좋다"는 <strong>권한
          위임</strong>이 본래 목적이죠. 그런데 다들 이걸 로그인(인증)에 쓰고 싶어 하니, OAuth2
          위에 <strong>인증 층을 표준으로 얹은 것</strong>이 <strong>OIDC(OpenID
          Connect)</strong>입니다. OIDC가 추가한 <span className="icode">id_token</span>은 그
          자체가 <strong>JWT</strong>여서 — 섹션 3에서 배운 서명 검증으로 "구글이 서명한 이
          사람"을 확인해요. 배운 조각들이 맞물리는 순간입니다.
        </p>
        <div className="tip">
          <b>소셜 로그인이 끝이 아니다</b> ⑤에서 구글 확인이 끝나면, 우리 서버는{' '}
          <strong>우리 자신의 액세스/리프레시 토큰을 새로 발급</strong>합니다. 구글의 토큰은
          "구글 자원에 접근할 권한"이지 우리 API의 통행증이 아니니까요. 즉 소셜 로그인은{' '}
          <strong>인증의 입구만 빌려오는 것</strong>이고, 입장 후의 세계는 섹션 4에서 만든 우리
          토큰 체계가 그대로 다스립니다.
        </div>
      </Section>

      <Section n={7} title="마이크로서비스 인증 — 신뢰를 어디에 배치하나" sub="게이트웨이 검증 vs 자체 검증, 그리고 X-Internal-Key">
        <p>
          서비스가 6개로 쪼개지면 새 질문이 생깁니다. <strong>JWT는 누가 검증하나?</strong>{' '}
          배치는 크게 두 가지예요. <strong>(A) 게이트웨이 집중 검증</strong> — 관문에서 한 번
          까고 뒤로는 신뢰를 전제하는 방식, <strong>(B) 서비스 자체 검증</strong> — 게이트웨이는
          길만 안내하고, 각 서비스가 같은 비밀키로 서명을 직접 확인하는 방식. 우리 플랫폼은{' '}
          <strong>(B)</strong>입니다.
        </p>
        <Code>{CODE_MSA}</Code>
        <p>
          (A)는 검증 로직이 한 곳이라 깔끔하지만, 게이트웨이 뒤의 내부망을 <strong>무조건
          신뢰</strong>하게 돼요. 게이트웨이가 뚫리거나 내부망에 다른 침입 경로가 생기면 모든
          서비스가 무방비가 되죠. (B)는 검증 코드가 서비스마다 복제되는 대신, <strong>각 서비스가
          자기 문 앞의 최종 관문</strong>이 됩니다 — "내부에서 온 요청도 검증한다"는 제로 트러스트
          방향의 선택이에요. JWT가 무상태라서 가능한 배치이기도 합니다. 세션이었다면 서비스 6개가
          전부 세션 저장소에 붙어야 했을 테니까요.
        </p>
        <p>
          사용자 없이 <strong>서비스끼리</strong> 부르는 호출은 결이 달라요. 예컨대 다른 서비스가
          auth의 <span className="icode">/internal/notifications</span>를 부를 때는 사용자 JWT가
          없으니, <span className="icode">X-Internal-Key</span>라는 <strong>공유 비밀 헤더</strong>로
          "나 동료 서비스야"를 증명합니다. 게이트웨이가 <span className="icode">/internal/**</span>{' '}
          경로를 외부에 아예 열지 않는 것과 겹쳐, 키 + 경로 차단의 <strong>이중 방어</strong>가
          되죠. 반대로 "이 요청을 시킨 <strong>사용자가 누구인지</strong>"가 뒤 서비스에도
          필요하면, JWT를 헤더에 실어 <strong>그대로 전파</strong>하고 받은 쪽이 다시 검증합니다.
        </p>
        <div className="tip">
          <b>실습 — 이 코스의 마무리</b> 우리 auth의 로그인→API 호출→액세스 만료→리프레시
          (회전)→로그아웃 흐름을 <strong>시퀀스 다이어그램</strong>으로 그려 보세요. 등장인물은
          브라우저·게이트웨이·auth·Valkey·일반 서비스 하나면 충분합니다. 다 그렸다면 같은
          시나리오를 <strong>세션 방식이었다면</strong>으로 한 장 더 그리고, 화살표 수·저장소
          조회 횟수·강제 로그아웃 지점을 비교해 보세요. 두 그림의 차이가 곧 이 코스에서 배운
          저울 — <strong>무상태의 자유와 대가</strong>입니다.
        </div>
      </Section>

      <div className="step-card">
        <div className="step-body">
          <h3 style={{ marginBottom: 6 }}>🔑 여기까지 왔다면</h3>
          <p className="muted">
            이제 "세션이냐 JWT냐"가 유행의 문제가 아니라 <strong>상태를 어디에 둘 것인가</strong>의
            문제라는 걸 알게 됐어요. 서버가 기억하는 세션의 통제력, 서명 하나로 달리는 JWT의
            확장성, 그 사이에서 리프레시 토큰과 Valkey로 균형을 잡은 우리 설계, 저장 위치와
            OAuth2·OIDC, 그리고 서비스 6개 사이의 신뢰 배치까지 — 인증이라는 축 하나로 아키텍처
            전체를 관통해 봤습니다. 그런데 이렇게 설계한 시스템이 <strong>실제로 잘 돌고
            있는지</strong>는 어떻게 알까요? 로그인 실패가 급증하면 누가 먼저 알아채야 할까요?
            다음 <Link to="/learn/lv3-observability"><strong>로깅·모니터링·알림 설계</strong></Link>{' '}
            코스에서, 돌아가는 시스템을 '보이게' 만드는 기술을 이어서 익혀 봅시다.
          </p>
        </div>
      </div>
    </div>
  );
}
