Node.js 교재 · 19편 · JWT 인증

JWT 인증 — 로그인·토큰 발급·검증

Auth.js 처럼 풀스택 솔루션이 못 닿는 곳 — 모바일 API·서드파티 발급의 표준.

JWT 토큰 구조와 로그인 흐름 컨셉 일러스트

Next.js 19편의 Auth.js 가 같은 사이트 안 인증의 답이라면, JWT(JSON Web Token) 는 그 범위 밖 — 모바일 앱 API, 외부 서비스 발급, 마이크로서비스 간 호출에서 표준. Node 직접 구현이 표준이다.

이번 편은 JWT 의 정체·로그인 흐름·실전 구현·refresh 패턴까지. 보안 사고 줄이는 5가지 함정도.

1. JWT 의 정체 — 점 두 개로 나뉜 문자열

JWT 는 그냥 3 부분이 점으로 연결된 문자열.

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMiLCJleHAiOjE3MTAwMDAwMDB9.X3sigX └─ header ─┘.└─────── payload ─────────┘.└─signature─┘

각 부분의 정체:

부분내용인코딩
header{"alg":"HS256"}Base64url
payload{"sub":"123","exp":1710000000,...}Base64url
signatureHMAC(header.payload, SECRET)Base64url
가장 큰 오해 — 암호화 아님 — JWT payload 는 그냥 Base64. 누구나 디코딩해 내용을 볼 수 있다 (11편 Buffer 챕터의 base64 toString 한 줄). signature 는 위변조 방지일 뿐, 비밀 데이터 숨기는 게 아님. 비밀번호·민감 정보를 payload 에 절대 넣지 말 것.

2. 설치와 헬퍼

$ npm install bcrypt jsonwebtoken $ npm install -D @types/bcrypt @types/jsonwebtoken
// auth/tokens.js import jwt from 'jsonwebtoken'; const SECRET = process.env.JWT_SECRET; // 17편 검증된 값 const EXP = process.env.JWT_EXPIRES || '15m'; export function signAccessToken(userId) { return jwt.sign({ sub: userId }, SECRET, { expiresIn: EXP }); } export function verifyToken(token) { try { return jwt.verify(token, SECRET); // 만료·서명 자동 검증 } catch { return null; } }

sign 으로 발급, verify 로 검증. 검증 실패(만료·잘못된 서명) 는 throw 라 try/catch 또는 null 반환.

3. 로그인·발급 — bcrypt 해시와 함께

비밀번호는 절대 평문 저장 금지. bcrypt 로 해시.

// 회원가입 import bcrypt from 'bcrypt'; import { pool } from '@/db'; router.post('/signup', async (req, res) => { const { email, password } = req.body; const hash = await bcrypt.hash(password, 12); // 12 라운드 권장 const { rows: [user] } = await pool.query( 'INSERT INTO users (email, password_hash) VALUES ($1, $2) RETURNING id, email', [email, hash] ); res.status(201).json(user); }); // 로그인 router.post('/login', async (req, res) => { const { email, password } = req.body; const { rows: [user] } = await pool.query( 'SELECT id, password_hash FROM users WHERE email = $1', [email] ); if (!user) return res.status(401).json({ error: '잘못된 자격' }); const valid = await bcrypt.compare(password, user.password_hash); if (!valid) return res.status(401).json({ error: '잘못된 자격' }); const token = signAccessToken(user.id); res.json({ token }); });
왜 bcrypt 12 라운드? — 라운드 수가 높을수록 무차별 대입 공격이 느려진다. 12 는 2026 기준 표준 (서버에서 ~250ms). 너무 낮으면(8) 보안 약함, 너무 높으면(15) 사용자가 로그인 1초 기다림. 12 가 스위트 스폿. 라이브러리 옵션 argon2id 가 더 안전한 후속이지만 bcrypt 가 호환성 좋음.

4. 검증 미들웨어 — Express 가드

14편의 미들웨어 패턴 응용.

// middleware/auth.js import { verifyToken } from './tokens.js'; export function requireAuth(req, res, next) { const auth = req.headers.authorization; if (!auth?.startsWith('Bearer ')) { return res.status(401).json({ error: '토큰 없음' }); } const payload = verifyToken(auth.slice(7)); if (!payload) { return res.status(401).json({ error: '만료 또는 잘못된 토큰' }); } req.user = { id: payload.sub }; next(); } // 사용 router.get('/me', requireAuth, (req, res) => { res.json({ id: req.user.id }); });

클라이언트는 매 요청 Authorization: Bearer eyJ... 헤더로 토큰을 보낸다. 표준 HTTP 인증 방식.

5. Refresh 토큰 — 짧은 access + 긴 refresh

한 토큰만 쓰면 — 길게 두면 탈취 위험, 짧게 두면 사용자가 매 시간 재로그인. 답이 2 토큰.

토큰수명저장 위치용도
Access15분메모리·localStorage매 API 요청
Refresh14일httpOnly 쿠키access 만료 시 갱신

access 가 만료되면 클라이언트가 POST /auth/refresh 호출 → 서버가 refresh 토큰(쿠키) 검증 → 새 access 발급. 탈취돼도 refresh 는 httpOnly 라 JS 가 못 읽어 XSS 로 못 빼간다.

// 로그인 시 둘 다 발급 const access = signAccessToken(user.id); // 15m const refresh = signRefreshToken(user.id); // 14d res.cookie('refreshToken', refresh, { httpOnly: true, secure: true, sameSite: 'strict', maxAge: 14 * 24 * 60 * 60 * 1000, }); res.json({ access }); // 본문엔 access 만
흔한 함정 5가지 — ① 비밀번호 평문 저장(범죄) ② payload 에 비밀 넣음(노출됨) ③ refresh 토큰 localStorage 저장(XSS 취약) ④ sameSite 안 설정(CSRF) ⑤ HS256 secret 짧음(32자+ 필수). 한 줄로 사고 가능. 처음부터 정석으로.

요약 — 19편 좌표

여기까지 정리. JWT 는 3부분 Base64 문자열 — payload 는 누구나 디코딩 가능, 비밀 X. 비밀번호는 bcrypt 12 라운드로 해시. 로그인 시 access(15분) + refresh(14일·httpOnly 쿠키) 두 토큰 발급. 미들웨어로 Authorization: Bearer 검증. 5가지 함정만 피하면 표준 인증 시스템 완성. 다음 편에서 파일 업로드로 멀티파트 폼.

다음 편 예고 — 파일 업로드

multer 로 이미지·파일 받기. 20편.

© 2026 주나이테크(주) @JUNAITECH