// 이 파일이 하는 일: "로그인 구현: 세션과 비밀번호 해시" 코스 — 인증/인가 구분에서 출발해,
// 비밀번호 해시(bcrypt)·회원가입 API·세션과 쿠키·로그인/로그아웃·인증 미들웨어·최소 방어까지
// 7개 섹션으로 안내하는 정적 학습 페이지. 프레이밍은 "내 서비스에 처음으로 '사용자'가 생기는 순간" —
// 기초 문법을 아는 학생이 블로그 API에 진짜 계정 시스템을 붙여 보게 하는 것이 목표다.
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 styles/global.css의
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용하고 색은 var(--*)만 쓴다(하드코딩 금지).
// 예제 코드는 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.

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

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

const CODE_FLOW = `[브라우저]                        [서버]                       [저장소]

① POST /signup ────────────▶  비밀번호를 해시해서 저장 ──▶  users 테이블
② POST /login  ────────────▶  해시 비교 → 세션 생성 ─────▶  세션 저장소
③ ◀── Set-Cookie: sid=abc1 ──  세션ID를 쿠키로 발급
④ GET /posts/3 ────────────▶  쿠키의 sid로 세션 조회
   (Cookie: sid=abc1)          → "아, 이 요청은 7번 사용자!"
⑤ POST /logout ────────────▶  세션 파기 + 쿠키 제거

인증(Authentication) = ①~④  "너 누구야?"를 확인하는 일
인가(Authorization)  = 그 뒤  "너 이거 해도 돼?"를 판정하는 일
→ 로그인은 인증이고, "내 글만 수정 가능"은 인가입니다. (섹션 6에서 구현)`;

const CODE_PLAINTEXT = `-- 절대 이렇게 저장하면 안 되는 users 테이블 (유출되는 순간 전원 피해)
 id | email             | password
----+-------------------+----------------
  1 | kim@example.com   | dlrjfdlaqlqjs1!   ← 평문 그대로
  2 | lee@example.com   | correct1234!
  3 | park@example.com  | correct1234!      ← 같은 비번인 것까지 다 보임

-- bcrypt 해시로 저장한 users 테이블 (유출돼도 원문을 알 수 없음)
 id | email             | password_hash
----+-------------------+------------------------------------------
  1 | kim@example.com   | $2b$10$N9qo8uLOickgx2ZMRZoMye3xLmN0...
  2 | lee@example.com   | $2b$10$dXJ3SW6G7P50lGmMkkmwe.aP4Zr9...
  3 | park@example.com  | $2b$10$Yk2QfCvyM1sD8hJp0aTzXOqWm3Lb...
                          ↑ 같은 비번(2·3번)인데도 해시가 다르다 = 솔트의 힘`;

const CODE_HASH = `const bcrypt = require('bcrypt');

// 해시는 '일방통행'이에요. 비밀번호 → 해시는 되지만, 해시 → 비밀번호는 불가능.
const hash1 = await bcrypt.hash('correct1234!', 10);
// '$2b$10$dXJ3SW6G7P50lGmMkkmwe.aP4Zr9uFq...'

// 같은 비밀번호를 한 번 더 해시하면?
const hash2 = await bcrypt.hash('correct1234!', 10);
// '$2b$10$Yk2QfCvyM1sD8hJp0aTzXOqWm3Lb0dE...'  ← 값이 다르다!

// 매번 다른 이유: bcrypt가 무작위 '솔트(salt)'를 만들어 비밀번호에 섞은 뒤
// 해시하고, 그 솔트를 해시 문자열 안에 함께 저장하기 때문이에요.
// 덕분에 "미리 계산해 둔 해시 사전(레인보 테이블)"으로 역추적하는 공격이 막힙니다.

// 그럼 검증은 어떻게? 저장된 해시에서 솔트를 꺼내 입력값을 같은 방식으로
// 해시해 본 뒤 비교합니다. 복원이 아니라 '재현해서 대조'하는 거예요.
await bcrypt.compare('correct1234!', hash1);   // true
await bcrypt.compare('wrongpass', hash1);      // false

// 둘째 인자 10은 cost(작업 횟수). 클수록 느려요 — 일부러 느리게 만들어서
// 공격자가 초당 수백만 개를 대입해 보는 걸 비싸게 만드는 설계입니다.`;

const CODE_SIGNUP = `// POST /api/signup — 회원가입: 중복 체크 → 해시 → 저장
app.post('/api/signup', async (req, res) => {
  const email = req.body.email;
  const password = req.body.password;
  if (!email || !password) {
    return res.status(400).json({ error: '이메일과 비밀번호는 필수입니다.' });
  }

  // ① 이메일 중복 체크 — 이미 있으면 409 Conflict
  const exists = await db.findUserByEmail(email);
  if (exists) {
    return res.status(409).json({ error: '이미 가입된 이메일입니다.' });
  }

  // ② 평문이 아니라 '해시'를 저장한다 (섹션 2에서 배운 그대로)
  const passwordHash = await bcrypt.hash(password, 10);
  const user = await db.createUser(email, passwordHash);

  // ③ 응답에 비밀번호(해시조차) 절대 담지 않는다 — 필요한 필드만 골라 보낸다
  res.status(201).json({ id: user.id, email: user.email });
});

// 나쁜 예: res.json(user)  ← user 객체를 통째로 보내면
// password_hash가 응답에 실려 나간다. '골라 담기'가 습관이 되어야 해요.`;

const CODE_SESSION = `// 세션 준비 — express-session이 '세션 저장소 + 쿠키 왕복'을 대신해 준다
const session = require('express-session');

app.use(session({
  secret: process.env.SESSION_SECRET,  // 쿠키 위조 방지 서명 키 (환경변수로!)
  resave: false,
  saveUninitialized: false,            // 로그인 전엔 세션을 만들지 않는다
  cookie: {
    httpOnly: true,                    // JS로 쿠키 접근 금지 (섹션 7)
    maxAge: 1000 * 60 * 60 * 2,        // 2시간
  },
}));

// 동작 원리 (왕복 구조):
// ① 로그인 성공 → 서버가 세션 저장소에 기록: sid 'abc1' = 사용자 7번
// ② 응답 헤더:   Set-Cookie: connect.sid=abc1
// ③ 브라우저가 쿠키를 저장하고, 이후 '같은 사이트' 요청마다 자동으로 실어 보냄
// ④ 서버는 쿠키 속 sid로 저장소를 뒤져 req.session.userId를 복원
//
// 핵심: 브라우저가 들고 다니는 건 '출입증 번호(세션ID)'뿐이고,
// 실제 정보(누구인지)는 서버 쪽 저장소에 있다는 것.`;

const CODE_LOGIN = `// POST /api/login — 로그인: 해시 비교 → 세션ID 재발급 → 세션에 기록
app.post('/api/login', async (req, res) => {
  const user = await db.findUserByEmail(req.body.email);

  // 사용자가 없어도, 비밀번호가 틀려도 → '같은 메시지'로 답한다 (아래 설명)
  const ok = user && (await bcrypt.compare(req.body.password, user.passwordHash));
  if (!ok) {
    return res.status(401).json({ error: '이메일 또는 비밀번호가 올바르지 않습니다.' });
  }

  // 로그인 성공 시 세션ID를 '새로' 발급 — 세션 고정 공격 방어 (섹션 7)
  req.session.regenerate((err) => {
    if (err) return res.status(500).json({ error: '로그인 처리에 실패했습니다.' });
    req.session.userId = user.id;              // 서버 쪽 기억: "이 세션 = 이 사용자"
    res.json({ id: user.id, email: user.email });
  });
});

// POST /api/logout — 로그아웃 = 서버 세션 파기 + 브라우저 쿠키 제거 (둘 다!)
app.post('/api/logout', (req, res) => {
  req.session.destroy(() => {                  // ① 서버 저장소에서 세션 삭제
    res.clearCookie('connect.sid');            // ② 브라우저에게 "쿠키 지워" 지시
    res.status(204).end();
  });
});`;

const CODE_AUTH_MW = `// 인증 미들웨어 — "로그인했는지" 검사를 한 곳으로 모은다
async function requireLogin(req, res, next) {
  if (!req.session.userId) {
    return res.status(401).json({ error: '로그인이 필요합니다.' });   // 인증 실패
  }
  // 이후의 모든 핸들러가 쓸 수 있게 사용자 정보를 실어 나른다
  req.user = await db.findUserById(req.session.userId);
  next();
}

// 보호 라우트 — 미들웨어를 끼우면 그 아래 코드는 '로그인된 상태'를 보장받는다
app.get('/api/me', requireLogin, (req, res) => {
  res.json({ id: req.user.id, email: req.user.email });
});

// 인가: "내 글만 수정 가능" — 소유권 검사. 로그인했어도 남의 글이면 403
app.put('/api/posts/:id', requireLogin, async (req, res) => {
  const post = await db.findPost(req.params.id);
  if (!post) return res.status(404).json({ error: '글이 없습니다.' });

  if (post.authorId !== req.user.id) {
    return res.status(403).json({ error: '본인 글만 수정할 수 있습니다.' }); // 인가 실패
  }
  const updated = await db.updatePost(post.id, req.body);
  res.json(updated);
});

// 401 vs 403 — 헷갈리면 이렇게 기억하세요.
// 401 Unauthorized: "너 누군지 모르겠어. 로그인부터 해." (인증 실패)
// 403 Forbidden:    "너 누군지는 아는데, 이건 네 권한 밖이야." (인가 실패)`;

const CODE_DEFENSE = `// 최소한의 방어 3종 — 완벽한 보안이 아니라 '기본기'입니다
// ① 쿠키 옵션: 세션ID가 새어 나갈 통로를 좁힌다
cookie: {
  httpOnly: true,   // document.cookie로 못 읽음 → 스크립트 주입(XSS)돼도 탈취 어려움
  secure: true,     // HTTPS에서만 전송 → 중간에서 엿보기(도청) 차단 (운영 환경 필수)
  sameSite: 'lax',  // 다른 사이트발 요청엔 쿠키를 잘 안 실음 → CSRF 완화
}

// ② 로그인 시도 제한: 무차별 대입(brute force)을 비싸게 만든다
// 아이디어: "같은 IP 또는 같은 계정이 5회 연속 실패하면 15분 잠금"
const fails = await store.get('login-fail:' + req.body.email);
if (fails >= 5) {
  return res.status(429).json({ error: '잠시 후 다시 시도해 주세요.' });
}
// 실패할 때마다 카운트 +1 (만료 15분), 성공하면 카운트 삭제

// ③ 세션ID 재발급: 로그인 '성공 직후' 반드시 새 세션ID로 갈아탄다
req.session.regenerate(() => { req.session.userId = user.id; });
// 공격자가 미리 심어 둔 세션ID를 사용자가 그대로 쓰게 만드는
// '세션 고정(session fixation)' 공격을 무력화합니다.`;

// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. 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: '회원가입 API' },
  { n: 4, label: '세션과 쿠키' },
  { n: 5, label: '로그인·로그아웃' },
  { n: 6, label: '미들웨어·보호 라우트' },
  { n: 7, label: '최소한의 방어' },
];

export default function Lv2AuthSessionHashPage() {
  return (
    <div>
      {/* 히어로: 이 코스가 어디서 출발해 어디까지 가는지 */}
      <div className="hero">
        <div className="eyebrow">Course · 중급 과정</div>
        <h1>🔐 로그인 구현<br />— 세션과 비밀번호 해시</h1>
        <p>
          지금까지 만든 API는 <strong>누가 요청해도 똑같이</strong> 응답했어요. 하지만 진짜
          서비스엔 '내 계정'이 있죠. 이 코스에서는 <strong>비밀번호를 지키는 법(해시)</strong>과{' '}
          <strong>사용자를 기억하는 법(세션)</strong>을 배워, 회원가입부터 "내 글만 수정 가능"
          까지 — 계정이 있는 서비스의 뼈대를 직접 세웁니다.
        </p>
        <div className="chip-row">
          <span className="chip">예상 소요 80분</span>
          <span className="chip">실습: 블로그 API에 로그인 붙이기</span>
          <span className="chip">선수 지식: HTTP·Express 기초</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="인증과 인가는 다르다" sub="'너 누구야?'와 '너 이거 해도 돼?'는 별개의 질문">
        <p>
          로그인을 구현하기 전에, 단어부터 정확히 잡고 갑시다. <strong>인증(Authentication)</strong>은
          "너 <strong>누구야</strong>?"를 확인하는 일이에요. 이메일과 비밀번호를 받아 "아, 김수습
          님이군요"라고 신원을 확인하는 것 — 로그인이 바로 인증입니다. <strong>인가(Authorization)</strong>는
          그 다음 질문, "너 이거 <strong>해도 돼</strong>?"예요. 로그인한 사용자라도 남의 글을
          지울 수는 없어야 하죠. 이 코스는 1~5섹션에서 인증을, 6섹션에서 인가를 구현합니다.
        </p>
        <p>
          그런데 왜 이게 어려운 문제일까요? <strong>HTTP는 기억력이 없기 때문</strong>이에요.
          요청 하나하나가 서로를 모르는 남남입니다. 1초 전에 로그인에 성공했어도, 다음 요청이
          오면 서버는 "누구세요?"부터 다시 시작해요. 이걸 <strong>무상태(stateless)</strong>라고
          부릅니다. 그래서 "로그인했다"는 상태를 어딘가에 <strong>이어 붙이는 장치</strong>가
          필요하고, 그 장치가 이 코스의 주인공인 <strong>세션과 쿠키</strong>예요.
        </p>
        <Code>{CODE_FLOW}</Code>
        <p>
          위 그림이 이 코스의 전체 지도입니다. ①회원가입(섹션 3) → ②~③로그인과 세션 발급(섹션
          4~5) → ④인증된 요청(섹션 6) → ⑤로그아웃(섹션 5). 각 화살표를 하나씩 코드로
          바꿔 나갈 거예요.
        </p>
        <div className="tip">
          <b>비유</b> 회사 건물을 떠올려 보세요. 1층 안내데스크에서 신분증을 확인받고{' '}
          <strong>출입증</strong>을 받는 게 <strong>인증</strong>, 그 출입증으로 열리는 문과 안
          열리는 문이 나뉘는 게 <strong>인가</strong>예요. 출입증이 있어도 서버실 문은 안 열리죠.
          로그인(인증)과 권한 검사(인가)는 이렇게 항상 한 쌍으로 다니지만 서로 다른 일입니다.
        </div>
      </Section>

      <Section n={2} title="비밀번호는 절대 그대로 저장하지 않는다" sub="해시는 일방통행 — 복원 불가가 '기능'이다">
        <p>
          회원가입 API를 만들기 전에 반드시 짚어야 할 철칙이 있어요.{' '}
          <strong>비밀번호를 평문(입력된 그대로)으로 저장하면 안 됩니다.</strong> 실제로 국내외
          유명 서비스들이 DB 유출 사고를 겪었고, 평문 저장이었던 곳은 사용자 전원의 비밀번호가
          그대로 공개됐어요. 더 무서운 건 <strong>비밀번호 재사용</strong>이에요 — 한 사이트에서
          털린 이메일·비밀번호 조합으로 다른 사이트 로그인을 시도하는 공격(크리덴셜 스터핑)이
          바로 이어집니다. 내 서비스의 부주의가 사용자의 <strong>다른 계정</strong>까지 위험하게
          만드는 거죠.
        </p>
        <Code>{CODE_PLAINTEXT}</Code>
        <p>
          해법은 <strong>해시(hash)</strong>입니다. 해시 함수는 입력을 고정 길이의 뒤죽박죽
          문자열로 바꾸는데, 결정적인 성질이 하나 있어요 — <strong>일방통행</strong>이라는 것.
          비밀번호 → 해시는 순식간이지만, 해시 → 비밀번호로 <strong>되돌리는 것은 불가능</strong>
          합니다. 암호화(encrypt)와 다른 점이 바로 여기예요. 암호화는 열쇠가 있으면 복호화가
          되지만, 해시는 애초에 되돌릴 수 없게 설계됐어요. 서버조차 사용자의 비밀번호 원문을
          모르는 상태 — 그게 <strong>목표</strong>입니다.
        </p>
        <Code>{CODE_HASH}</Code>
        <p>
          위 코드에서 눈여겨볼 것 두 가지. 첫째, <strong>같은 비밀번호인데 해시가 매번 다릅니다.</strong>{' '}
          bcrypt가 무작위 <strong>솔트(salt)</strong>를 섞기 때문인데, 덕분에 공격자가 "자주 쓰는
          비밀번호 100만 개의 해시"를 미리 계산해 와도(레인보 테이블) 우리 DB엔 안 통해요.
          둘째, bcrypt는 <strong>일부러 느립니다.</strong> cost 값(예제의 10)을 올릴수록 한 번
          해시에 더 오래 걸려서, 초당 수백만 번 대입해 보려는 공격자의 비용이 폭증해요.
          "느린 게 장점"인 흔치 않은 함수입니다.
        </p>
        <div className="tip">
          <b>그럼 검증은 어떻게?</b> 복원이 안 되는데 로그인 때 비밀번호가 맞는지 어떻게 확인할까요?
          방향을 뒤집으면 됩니다. 사용자가 입력한 값을 <strong>같은 솔트로 다시 해시해서</strong>{' '}
          저장된 해시와 <strong>비교</strong>하는 거예요. <span className="icode">bcrypt.compare(입력값, 저장된해시)</span>가
          그 일을 해 줍니다. '풀어서 맞추기'가 아니라 '재현해서 대조하기' — 이 감각이 해시
          이해의 핵심이에요.
        </div>
      </Section>

      <Section n={3} title="회원가입 API 만들기" sub="중복 체크 → 해시 → 저장, 그리고 응답은 골라 담기">
        <p>
          이제 첫 코드를 짤 차례예요. 회원가입 API의 뼈대는 세 단계입니다.{' '}
          <strong>① 이메일 중복 체크</strong> — 이미 가입된 이메일이면{' '}
          <span className="icode">409 Conflict</span>로 거절합니다. 409는 "요청 자체는 이해했지만
          현재 상태와 <strong>충돌</strong>한다"는 뜻으로, 중복 자원에 딱 맞는 상태 코드예요.{' '}
          <strong>② 해시해서 저장</strong> — 섹션 2에서 배운{' '}
          <span className="icode">bcrypt.hash</span>로 변환한 값만 DB에 넣습니다.{' '}
          <strong>③ 응답 골라 담기</strong> — 성공 응답(<span className="icode">201 Created</span>)에
          비밀번호 관련 필드는 절대 싣지 않아요.
        </p>
        <Code>{CODE_SIGNUP}</Code>
        <p>
          ③이 사소해 보여도 실무 사고의 단골이에요. <span className="icode">res.json(user)</span>처럼
          DB에서 꺼낸 객체를 <strong>통째로</strong> 응답에 넘기면, 지금은 괜찮아 보여도 나중에
          누가 컬럼을 추가하는 순간 그 값까지 줄줄 새어 나갑니다. "응답엔 필요한 필드만{' '}
          <strong>골라 담는다</strong>"를 처음부터 손에 익히세요 — 해시조차 밖으로 내보낼 이유가
          없습니다.
        </p>
        <div className="tip">
          <b>직접 확인해 보기</b> 실습 프로젝트에서 같은 이메일로 회원가입을 <strong>두 번</strong>{' '}
          요청해 보세요. 첫 번째는 <span className="icode">201</span>, 두 번째는{' '}
          <span className="icode">409</span>가 와야 정상입니다. 그리고 DB(또는 저장 파일)를 열어{' '}
          <span className="icode">$2b$10$...</span>로 시작하는 해시가 저장됐는지, 평문이 어디에도
          남아 있지 않은지 눈으로 확인하세요.
        </div>
      </Section>

      <Section n={4} title="세션과 쿠키의 동작 원리" sub="브라우저는 출입증 번호만, 실제 기억은 서버가">
        <p>
          섹션 1에서 봤듯 HTTP는 기억력이 없어요. 그래서 로그인 성공의 '증표'를 매 요청에 실어
          보내야 하는데, 그 왕복 구조가 <strong>세션 + 쿠키</strong>입니다. 흐름은 이래요.
          로그인에 성공하면 서버가 <strong>세션 저장소</strong>에 "세션ID <span className="icode">abc1</span>{' '}
          = 사용자 7번"이라고 기록하고, 응답 헤더 <span className="icode">Set-Cookie</span>로 그{' '}
          <strong>세션ID만</strong> 브라우저에 건넵니다. 브라우저는 받은 쿠키를 저장해 뒀다가,
          이후 같은 사이트로 가는 <strong>모든 요청에 자동으로</strong> 실어 보내요. 서버는 쿠키
          속 세션ID로 저장소를 뒤져 "아, 7번 사용자구나"를 복원합니다.
        </p>
        <Code>{CODE_SESSION}</Code>
        <p>
          여기서 역할 분담이 중요해요. <strong>브라우저가 들고 다니는 건 뜻 없는 무작위 문자열
          (세션ID)뿐</strong>이고, "그게 누구인지"라는 실제 정보는 <strong>서버 쪽 세션 저장소</strong>에
          있습니다. 마치 코트 보관소의 번호표처럼요 — 번호표 자체엔 코트에 대한 정보가 하나도
          없죠. 대신 이 구조의 대가로, 서버는 로그인한 사용자 수만큼 세션을 <strong>기억하고
          있어야</strong> 합니다(상태를 서버가 짊어지는 방식). 저장소는 개발 단계에선 메모리로
          충분하고, 서비스가 커지면 Redis 같은 별도 저장소로 옮깁니다.
        </p>
        <div className="tip">
          <b>직접 확인해 보기</b> 브라우저 개발자도구(<span className="icode">F12</span>)의{' '}
          <span className="icode">Application → Cookies</span>를 열어 두고 로그인해 보세요.{' '}
          <span className="icode">connect.sid</span> 같은 이름의 쿠키가 <strong>생기는 순간</strong>을
          직접 볼 수 있습니다. 값을 복사해 보면 뜻을 알 수 없는 문자열일 거예요 — 그게
          정상입니다(번호표니까요). 그리고 <span className="icode">Network</span> 탭에서 로그인
          응답의 <span className="icode">Set-Cookie</span> 헤더와, 다음 요청에 자동으로 붙는{' '}
          <span className="icode">Cookie</span> 헤더를 찾아 왕복 구조를 눈으로 확인하세요.
        </div>
      </Section>

      <Section n={5} title="로그인·로그아웃 구현" sub="compare로 검증하고, 파기는 서버·브라우저 양쪽에서">
        <p>
          재료가 다 모였어요. 로그인은 <strong>① 이메일로 사용자 찾기 → ② <span className="icode">bcrypt.compare</span>로
          비밀번호 대조 → ③ 세션에 사용자 기록</strong> 순서입니다. 로그아웃은 그 반대로,
          서버의 세션을 <strong>파기</strong>하고 브라우저의 쿠키도 <strong>지우게</strong> 하는
          것까지가 한 세트예요.
        </p>
        <Code>{CODE_LOGIN}</Code>
        <p>
          코드에서 일부러 그렇게 짠 부분이 하나 있어요. 이메일이 틀렸든 비밀번호가 틀렸든{' '}
          <strong>똑같이 "이메일 또는 비밀번호가 올바르지 않습니다"</strong>라고 답합니다. 왜
          친절하게 구분해 주면 안 될까요? "비밀번호가 틀렸습니다"라고 알려주는 순간, 공격자에게{' '}
          <strong>"이 이메일은 가입돼 있다"</strong>는 사실을 공짜로 확인시켜 주기 때문이에요.
          그 목록을 모아 비밀번호 대입 공격의 표적을 좁히죠. 에러 메시지는 사용자에겐 조금
          불친절해도, 공격자에게 <strong>정보를 흘리지 않는 쪽</strong>이 원칙입니다.
        </p>
        <p>
          로그아웃도 뜯어볼게요. <span className="icode">req.session.destroy()</span>만 하고 쿠키를
          안 지우면 브라우저에 죽은 번호표가 남고, 반대로 쿠키만 지우고 서버 세션을 안 지우면{' '}
          <strong>서버엔 여전히 유효한 세션이 살아 있어요.</strong> 누군가 그 세션ID를 알고 있다면
          로그아웃했다고 믿는 사용자 행세를 계속할 수 있습니다. 그래서 로그아웃 ={' '}
          <strong>서버 세션 파기 + 쿠키 제거, 반드시 둘 다</strong>입니다.
        </p>
        <div className="tip">
          <b>왜 로그인 성공 시 regenerate?</b> 코드의 <span className="icode">req.session.regenerate()</span>는
          로그인 성공 직후 세션ID를 <strong>새것으로 교체</strong>합니다. 로그인 전에 쓰던
          세션ID를 그대로 승격시키면, 공격자가 미리 심어 둔 세션ID로 로그인 상태를 가로채는{' '}
          <strong>세션 고정 공격</strong>이 가능해져요. 자세한 원리는 섹션 7에서 다룹니다 —
          지금은 "성공하면 번호표를 새로 발급한다"만 기억하세요.
        </div>
      </Section>

      <Section n={6} title="인증 미들웨어와 보호 라우트" sub="검사는 한 곳에서, 사용자 정보는 req.user에 실어서">
        <p>
          이제 "로그인한 사람만" 쓸 수 있는 API를 만들 차례예요. 순진하게 만들면 모든 핸들러
          첫 줄마다 <span className="icode">if (!req.session.userId) ...</span>를 복사하게 되는데,
          라우트가 10개면 10번 복붙이고 하나라도 빠뜨리면 그대로 구멍입니다. Express의{' '}
          <strong>미들웨어</strong>가 정확히 이 문제를 풉니다 — 검사를 <strong>함수 하나로
          모으고</strong>, 보호가 필요한 라우트에 끼워 넣는 거예요.
        </p>
        <Code>{CODE_AUTH_MW}</Code>
        <p>
          <span className="icode">requireLogin</span>이 하는 일은 두 가지입니다. 세션에 사용자가
          없으면 <span className="icode">401</span>로 끊어서 <strong>401 처리를 일원화</strong>하고,
          있으면 DB에서 사용자를 찾아 <strong><span className="icode">req.user</span>에 실어</strong>{' '}
          다음 핸들러로 넘겨요(<span className="icode">next()</span>). 덕분에 뒤쪽 핸들러들은
          "여기 도달했다 = 로그인된 상태다"를 보장받고, <span className="icode">req.user.id</span>를
          바로 꺼내 쓸 수 있습니다.
        </p>
        <p>
          그리고 드디어 <strong>인가</strong>가 등장합니다. 글 수정 라우트를 보세요 — 로그인
          검사(인증)를 통과했어도, <span className="icode">post.authorId !== req.user.id</span>면{' '}
          <span className="icode">403 Forbidden</span>으로 거절해요. 이게 <strong>소유권 검사</strong>입니다.
          "누구냐"(401 문제)와 "네 것이냐"(403 문제)가 코드에서 어떻게 다른 층으로 갈라지는지,
          섹션 1의 구분이 여기서 실물이 됩니다.
        </p>
        <div className="tip">
          <b>흔한 실수</b> 프론트엔드에서 "수정 버튼을 숨겼으니까 됐다"고 생각하는 것.
          버튼을 숨겨도 <strong>API는 curl이나 개발자도구로 직접 호출</strong>할 수 있어요.
          인가 검사는 반드시 <strong>서버에서</strong> 해야 합니다. 프론트의 버튼 숨김은 UX일 뿐,
          보안이 아니에요.
        </div>
      </Section>

      <Section n={7} title="최소한의 방어" sub="쿠키 옵션·시도 제한·세션 재발급 — 기본기 3종">
        <p>
          완벽한 보안은 이 코스의 범위를 넘지만, <strong>이것조차 없으면 곤란한 기본기 세 가지</strong>는
          챙기고 마칩시다. 첫째, <strong>쿠키 옵션</strong>. 세션ID는 곧 출입증이라 새어 나가면
          끝이에요. <span className="icode">httpOnly</span>는 페이지의 자바스크립트가{' '}
          <span className="icode">document.cookie</span>로 쿠키를 읽지 못하게 막아서, 악성
          스크립트가 주입돼도(XSS) 세션ID 탈취를 어렵게 합니다.{' '}
          <span className="icode">secure</span>는 HTTPS 연결에서만 쿠키를 전송하게 해서 중간
          도청을 차단하고요.
        </p>
        <Code>{CODE_DEFENSE}</Code>
        <p>
          둘째, <strong>로그인 시도 제한</strong>. 공격자는 자주 쓰는 비밀번호 목록을 기계적으로
          대입합니다(무차별 대입, brute force). bcrypt가 한 번 한 번을 느리게 만들어 주지만,
          서버 차원에서도 "5회 연속 실패 → 잠시 잠금(<span className="icode">429</span>)" 같은
          제한을 걸면 공격 비용이 훨씬 올라가요. 실패 횟수를 세고 성공하면 지우는, 단순한
          카운터로 시작하면 됩니다.
        </p>
        <p>
          셋째, <strong>세션ID 재발급</strong>. 섹션 5에서 예고한 <strong>세션 고정(session
          fixation)</strong> 공격은 이렇게 흘러가요 — 공격자가 세션ID 하나를 미리 확보해 피해자의
          브라우저에 심어 둡니다 → 피해자가 그 세션ID를 단 채로 로그인합니다 → 서버가 그 세션을
          그대로 "로그인됨"으로 승격시키면, <strong>같은 세션ID를 아는 공격자도 로그인 상태</strong>가
          돼요. 방어는 간단합니다. 로그인 <strong>성공 직후 세션ID를 새로 발급</strong>
          (<span className="icode">regenerate</span>)하면, 공격자가 심어 둔 옛 ID는 휴지 조각이
          됩니다.
        </p>
        <div className="tip">
          <b>과장 금지</b> 이 세 가지로 "보안 끝"이 아니에요. CSRF 토큰, 비밀번호 정책, 2단계
          인증 등 지형은 훨씬 넓습니다. 다만 순서가 중요해요 — <strong>평문 저장 안 하기,
          에러 메시지로 정보 안 흘리기, 쿠키 옵션, 시도 제한, 세션 재발급.</strong> 이 기본기가
          없는 서비스에 고급 기법을 얹는 건 문이 열린 집에 CCTV만 다는 격입니다.
        </div>
      </Section>

      <div className="step-card">
        <div className="step-body">
          <h3 style={{ marginBottom: 6 }}>🔐 여기까지 왔다면</h3>
          <p className="muted">
            이제 여러분의 블로그 API엔 <strong>계정</strong>이 생겼습니다. 비밀번호는 해시로만
            저장되고(솔트 덕에 같은 비번도 다른 해시), 로그인하면 세션ID라는 번호표가 쿠키로
            오가고, 보호 라우트는 401로, 남의 글은 403으로 끊어 내죠. "누구냐(인증)"와
            "해도 되냐(인가)"를 코드로 갈라 본 경험은 앞으로 어떤 프레임워크를 만나도 그대로
            통합니다. 다음은 화면 차례예요 — 로그인 폼의 입력값은 어디에 두고, "로그인됨"
            상태는 화면에 어떻게 반영할까요? 다음{' '}
            <Link to="/learn/lv2-react-state-effects"><strong>React 상태와 이펙트 실전</strong></Link>{' '}
            코스에서, 오늘 만든 API에 살아 움직이는 프론트엔드를 붙여 봅시다.
          </p>
        </div>
      </div>
    </div>
  );
}
