
바이브 코딩으로 JWT 인증 구현할 때, 반드시 챙겨야 할 것들
바이브 코딩으로 JWT 인증 구현할 때, 반드시 챙겨야 할 것들

"로그인 기능 만들어줘" — 바이브 코딩에서 가장 흔한 요청 중 하나입니다. AI는 금방 만들어줍니다. 하지만 그렇게 만든 로그인, 정말 안전할까요?인증(Authentication)은 서비스의 대문입니다. 대문이 허술하면 아무나 들어옵니다. 사용자 정보 유출, 계정 탈취, 데이터 삭제 — 모두 인증이 뚫리면 일어나는 일입니다.
이 글은 바이브 코딩으로 JWT 인증을 구현할 때, 프롬프트 하나에 어떤 조건들을 담아야 하는지, 그리고 왜 그 조건들이 필요한지를 설명합니다.
JWT란? 30초 정리
JWT(JSON Web Token)는 사용자 인증 정보를 담은 암호화된 문자열입니다.
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiIxMjMiLCJyb2xlIjoiYWRtaW4ifQ.abc123...
이 문자열 안에 "이 사람은 userId=123이고, role=admin이야"라는 정보가 들어있습니다. 서버는 이 토큰을 검증해서 "아, 로그인한 사용자구나"라고 판단합니다.
왜 JWT를 쓰나?
| 방식 | 장점 | 단점 |
|---|---|---|
| 세션 (서버 저장) | 서버에서 세션 삭제 = 즉시 로그아웃 | 서버 메모리 사용, 서버 여러 대일 때 공유 어려움 |
| JWT (토큰) | 서버 부담 적음, 확장 용이 | 한 번 발급하면 만료 전까지 유효 |
JWT의 가장 큰 문제: 발급된 토큰을 서버가 직접 무효화하기 어렵다.것이 이 글에서 다루는 모든 보안 조치의 출발점입니다.
인증이 뚫리면 무슨 일이 벌어지나?
"개인 프로젝트인데 보안까지 신경 써야 해?" — 네, 반드시요.### 실제 발생 가능한 시나리오
시나리오 1: 토큰 탈취```
- 사용자가 카페 와이파이에서 로그인
- 공격자가 네트워크를 감청하여 토큰 획득
- HTTPS가 아니면 토큰이 평문으로 노출
- 공격자가 토큰으로 사용자 행세 → 데이터 접근/수정/삭제
시나리오 2: XSS 공격```
1. 토큰을 localStorage에 저장
2. 공격자가 게시글에 악성 스크립트 삽입
3. 다른 사용자가 글을 읽으면 스크립트 실행
4. localStorage의 토큰을 공격자 서버로 전송
5. 공격자가 해당 사용자의 계정을 완전히 장악
시나리오 3: 브루트포스```
- 공격자가 자동화 도구로 비밀번호 조합을 반복 시도
- 로그인 시도 횟수 제한이 없으면 수천~수만 번 시도 가능
- 간단한 비밀번호는 몇 분 안에 뚫림
런 걸 방지하는 조건들을 프롬프트에 넣어야 합니다.---
## 체크리스트: 프롬프트에 반드시 넣어야 할 조건들

---
## 1. Access Token + Refresh Token 분리
### 왜 필요한가?
토큰 하나로 모든 걸 처리하면:
- 유효기간을 길게 → 탈취 시 오래 악용됨
- 유효기간을 짧게 → 사용자가 자주 재로그인해야 함
해결: 역할을 나누자| | Access Token | Refresh Token |
|---|---|---|
| 목적 | API 인증 | Access Token 갱신 |
| 유효기간 | 15분| 7일|
| 저장 | httpOnly 쿠키 | httpOnly 쿠키 + DB |
| 탈취 위험 | 짧은 시간만 유효 | DB에서 즉시 삭제 가능 |
### 프롬프트에 이렇게 넣으세요
Access Token: 유효기간 15분, httpOnly 쿠키, payload에 userId/role/deviceId Refresh Token: 유효기간 7일, httpOnly 쿠키 + DB 저장
---
## 2. Refresh Token Rotation
### 왜 필요한가?
Refresh Token이 탈취되면 7일간 새 Access Token을 무한히 발급받을 수 있습니다.
Rotation: Refresh Token을 사용할 때마다 새 Refresh Token을 발급하고, 이전 것을 무효화합니다.
1회차: Refresh-A로 갱신 → Access-2 + Refresh-B 발급, Refresh-A 삭제 2회차: Refresh-B로 갱신 → Access-3 + Refresh-C 발급, Refresh-B 삭제
만약 공격자가 Refresh-A를 탈취해서 사용하면? → Refresh-A는 이미 삭제됨 → 실패 → 탈취 감지 → 해당 사용자의 모든 토큰 무효화
### 프롬프트에 이렇게 넣으세요
Refresh Token 사용 시 Rotation 적용. 새 Refresh Token 발급하고 이전 것은 DB에서 삭제. 이미 삭제된 토큰으로 요청이 오면 해당 사용자의 모든 세션 무효화 (탈취 의심).
---
## 3. 토큰 블랙리스트 (즉시 무력화)
### 왜 필요한가?
사용자가 로그아웃했는데, Access Token의 유효기간(15분)이 남아있으면? 그 토큰으로 여전히 API를 호출할 수 있습니다.블랙리스트: 무효화된 토큰을 목록에 등록하고, 매 요청마다 체크합니다.
### 프롬프트에 이렇게 넣으세요
로그아웃 시:
- Refresh Token: DB에서 삭제
- Access Token: 블랙리스트 테이블에 추가 (만료 시간까지만 보관)
비밀번호 변경 시:
- 해당 사용자의 모든 Refresh Token 삭제
- 현재 Access Token 블랙리스트에 추가
---
## 4. 토큰 저장 위치 (XSS 방어)
### 왜 필요한가?
❌ localStorage → JavaScript로 접근 가능 → XSS 공격에 토큰 탈취 ❌ sessionStorage → 같은 문제 ✅ httpOnly 쿠키 → JavaScript로 접근 불가 → XSS에 안전
httpOnly 쿠키는 브라우저가 자동으로 요청에 포함하지만, JavaScript에서는 읽을 수 없습니다.
### 프롬프트에 이렇게 넣으세요
토큰을 localStorage에 절대 저장하지 마. httpOnly 쿠키로 전달 (Secure, SameSite=Strict). CSRF 방어: SameSite=Strict + Origin 헤더 검증.
---
## 5. 로그인 시도 제한 & 계정 잠금
### 왜 필요한가?
로그인 시도 횟수 제한이 없으면, 공격자는 자동화 도구로 수만 가지 비밀번호를 시도할 수 있습니다(브루트포스 공격).
### 프롬프트에 이렇게 넣으세요
login_attempts 테이블로 모든 로그인 시도 기록 (성공/실패, IP, UserAgent).
실패 횟수 관리:
- 최근 30분 내 5회 연속 실패 → 계정 15분 잠금
- 10회 연속 실패 → 1시간 잠금 + 관리자 알림
users 테이블에 status(ACTIVE/LOCKED/BLOCKED), lockedUntil, failedAttempts 추가.
잠금 상태 응답: "인증에 실패했습니다" (남은 시간, 실패 횟수 등 상세 정보 노출 금지)
### 왜 상세 정보를 숨기나?
- "비밀번호가 틀렸습니다" → 공격자: "아, 이 이메일은 가입되어 있구나"
- "3회 남았습니다" → 공격자: "좋아, 아직 기회가 있네"
- "인증에 실패했습니다"→ 공격자: 아무 정보도 얻지 못함
---
## 6. 접속 디바이스 관리
### 왜 필요한가?
누군가 내 계정으로 로그인하고 있다면? 내가 어디서 로그인되어 있는지 확인하고, 의심스러운 기기를 강제 로그아웃할 수 있어야 합니다. (네이버, 구글, 카카오 모두 이 기능을 제공합니다)
### 프롬프트에 이렇게 넣으세요
devices 테이블: userId, deviceName, userAgent, ip, lastActive
로그인 시 디바이스 정보 기록. 최대 동시 접속 5대, 초과 시 가장 오래된 세션 자동 만료.
API:
- GET /api/auth/devices — 내 접속 기기 목록
- DELETE /api/auth/devices/{id} — 특정 기기 세션 종료
- POST /api/auth/logout-all — 모든 기기 로그아웃
새로운 디바이스/IP에서 로그인 시 알림 (이메일 또는 텔레그램).
---
## 7. 감사 로그 (Audit Log)
### 왜 필요한가?
보안 사고가 발생했을 때 "언제, 누가, 어디서, 무엇을 했는지"추적할 수 없으면, 원인 파악도 대응도 불가능합니다.
### 프롬프트에 이렇게 넣으세요
audit_logs 테이블: userId, action, ip, userAgent, metadata(JSON), createdAt
기록 대상:
- LOGIN_SUCCESS, LOGIN_FAIL, LOGOUT
- PASSWORD_CHANGE, PASSWORD_RESET_REQUEST
- TOKEN_REFRESH, TOKEN_REVOKE
- DEVICE_REMOVE, SESSION_FORCE_LOGOUT
- ACCOUNT_LOCK, ACCOUNT_UNLOCK
관리자 페이지에서 사용자별 접근 기록 조회 가능. 30일 이상 된 로그는 아카이브.
---
## 8. 비밀번호 정책
### 왜 필요한가?
"1234", "password", "qwerty" — 이런 비밀번호는 브루트포스 없이도 몇 초면 뚫립니다.
### 프롬프트에 이렇게 넣으세요
비밀번호 정책:
- 최소 8자, 대문자+소문자+숫자+특수문자 각 1개 이상
- 최근 3개 비밀번호 재사용 금지 (password_history 테이블)
- 비밀번호 변경 시 모든 기존 세션 무효화
- 비밀번호는 BCrypt로 해싱 (평문 저장 절대 금지)
---
## 9. 보안 키 관리
### 왜 필요한가?
JWT를 서명하는 Secret Key가 유출되면, 공격자가 마음대로 토큰을 만들 수 있습니다. 관리자 토큰도 만들 수 있습니다.
### 프롬프트에 이렇게 넣으세요
보안 키 관리:
- ACCESS_SECRET, REFRESH_SECRET 분리 (하나가 유출돼도 다른 하나는 안전)
- 최소 256비트 랜덤 문자열
- 환경변수(.env)로 관리, 코드에 하드코딩 절대 금지
- 키 로테이션 지원: PREV_SECRET 환경변수로 이전 키 검증 가능
---
## 완성 프롬프트: Next.js 버전
아래 프롬프트를 그대로 AI에게 전달하면, 위의 모든 보안 조건이 반영된 인증 시스템을 구현할 수 있습니다.
JWT 기반 인증/인가 시스템을 구현해줘.
[기술 스택] Next.js (App Router) + TypeScript + PostgreSQL + Prisma + jose
[토큰 구조]
- Access Token: 유효기간 15분, payload(userId, role, deviceId), httpOnly 쿠키
- Refresh Token: 유효기간 7일, DB 저장 + httpOnly 쿠키, 사용 시 Rotation
[토큰 무력화]
- 로그아웃: refresh token DB 삭제 + access token 블랙리스트
- 비밀번호 변경: 해당 사용자 모든 토큰 무효화
- 이미 삭제된 refresh token 사용 시 → 전체 세션 무효화 (탈취 의심)
[계정 보호]
- login_attempts 테이블로 모든 시도 기록
- 5회 실패 → 15분 잠금, 10회 → 1시간 잠금 + 관리자 알림
- users에 status(ACTIVE/LOCKED/BLOCKED), lockedUntil, failedAttempts
- 비밀번호: 8자+복합, BCrypt 해싱, 최근 3개 재사용 금지
[디바이스 관리]
- devices 테이블: userId, deviceName, userAgent, ip, lastActive
- 최대 동시 접속 5대, 초과 시 오래된 세션 자동 만료
- API: 기기 목록 조회, 특정 기기 종료, 전체 로그아웃
[감사 로그]
- audit_logs: userId, action, ip, userAgent, metadata, createdAt
- LOGIN_SUCCESS/FAIL, LOGOUT, PASSWORD_CHANGE, TOKEN_REVOKE 등 기록
[보안]
- httpOnly + Secure + SameSite=Strict 쿠키
- CSRF: Origin 헤더 검증
- 키 분리: ACCESS_SECRET, REFRESH_SECRET
- 에러 응답 통일: "인증에 실패했습니다"
[구현 범위] Prisma 스키마, JWT 유틸, middleware.ts, API 라우트 (login, logout, refresh, devices, logout-all)
---
## 완성 프롬프트: Spring Boot 버전
JWT 기반 인증/인가 시스템을 구현해줘.
[기술 스택] Spring Boot 3.x + Java 17 + Spring Security 6 + PostgreSQL + JPA + jjwt
[토큰 구조]
- Access Token: 15분, Authorization Bearer 헤더
- Refresh Token: 7일, DB 저장 + httpOnly 쿠키, 사용 시 Rotation
[토큰 무력화]
- 로그아웃: refresh token DB 삭제 + access token 블랙리스트 (Redis 우선, 없으면 DB + 스케줄러 정리)
- 비밀번호 변경: 모든 토큰 무효화
- 삭제된 refresh token 재사용 → 전체 세션 무효화
[계정 보호]
- login_attempts 테이블로 모든 시도 기록
- 5회 실패 → 15분 잠금, 10회 → 1시간 잠금 + 관리자 알림
- User 엔티티에 status, lockedUntil, failedAttempts
- BCryptPasswordEncoder, 최근 3개 재사용 금지
[디바이스 관리]
- Device 엔티티: userId, deviceName, userAgent, ip, lastActive
- 최대 동시 접속 5대
- Controller: 기기 목록, 특정 기기 종료, 전체 로그아웃
[감사 로그]
- AuditLog 엔티티: userId, action, ip, userAgent, metadata, createdAt
- @Scheduled로 30일 이상 로그 아카이브
[보안]
- CORS: 허용 도메인 명시, credentials=true
- SecurityConfig: 필터 체인, 경로별 권한
- jwt.access-secret / jwt.refresh-secret 분리
- AuthenticationEntryPoint 커스텀: 통일된 에러 응답
[구현 범위] Entity + Repository, JwtTokenProvider, JwtAuthenticationFilter, SecurityConfig, AuthController, AuthService
---
## 프롬프트를 줄 때 핵심 원칙
### 1. "보안"을 빠뜨리지 마세요
AI에게 "로그인 만들어줘"라고만 하면, 기본적인 로그인/로그아웃만 만들어줍니다. 보안 조건을 명시하지 않으면 AI는 알아서 챙기지 않습니다.### 2. "왜"를 이해하고 있어야 합니다
각 조건이 왜 필요한지 이해하고 있어야:
- AI가 잘못 구현했을 때 알아차릴 수 있고- 요구사항이 바뀌었을 때 떤 조건을 조정해야 하는지판단할 수 있습니다
### 3. 한 번에 다 넣으세요
보안은 나중에 추가하기 어렵습니다.처음부터 전체 구조를 잡고 시작하는 것이 훨씬 효율적입니다. 위 프롬프트를 처음에 한 번 전달하면, AI가 전체 구조를 고려해서 설계합니다.
---
## 마치며
인증 시스템은 서비스의 둥입니다. 기둥이 부실하면 나머지가 아무리 잘 만들어져도 소용없습니다.
바이브 코딩의 장점은 복잡한 구현을 AI가 해준다는 것입니다. 하지만 무엇을 만들어야 하는지는 당신이 알아야 합니다. 이 글의 체크리스트와 프롬프트를 활용하면, 코드를 직접 쓰지 않아도 전문가 수준의 인증 시스템을 구축할 수 있습니다.
보안은 타협의 대상이 아닙니다. 처음부터 제대로 만드세요.

