
HTTP는 stateless이기 때문에, 보호된 리소스에 접근할 때마다 인증 과정이 필요하다. 이 인증 정보는 Header에 담긴다.
자격증명 헤더는 방향에 따라 두 가지로 나뉜다.
| 헤더 | 방향 | 역할 |
|---|---|---|
WWW-Authenticate | 서버 → 클라이언트 | 인증이 필요하다고 알리는 헤더 (401과 함께 전송) |
Authorization | 클라이언트 → 서버 | 인증 정보를 담아 보내는 헤더 |
인증 흐름:
1. 클라이언트 → 서버 "이 리소스 줘"
2. 서버 → 클라이언트 401 + WWW-Authenticate: "Basic으로 인증해"
3. 클라이언트 → 서버 Authorization: Basic [인코딩된 정보]
4. 서버 → 클라이언트 인증 성공 → 리소스 응답
| 방식 | 설명 |
|---|---|
| Basic | ID:PW를 Base64 인코딩해서 전송 |
| Digest | PW를 해시로 변환해서 전송 (Basic보다 안전) |
| Bearer | 토큰(JWT 등)을 전송 |
| OAuth | 외부 서비스 인증 위임 (구글 로그인 등) |
ID, PW를 Base64 인코딩해서 헤더에 담아 전송하는 가장 단순한 인증 방식이다.
user:password
↓ Base64 인코딩
dXNlcjpwYXNzd29yZA==
↓ 헤더에 담아 전송
Authorization: Basic dXNlcjpwYXNzd29yZA==
Base64는 암호화가 아니라 인코딩이다. 누구나 디코딩 가능하므로 보안용이 아닌 전송용이다.
Basic이 매 요청마다 PW를 헤더에 담아서 보내는 문제를 해결하기 위해 등장했다. PW 대신 해시값을 전송한다.
Basic: user:password → Base64 인코딩 → 전송 (사실상 평문)
Digest: password → 해시(Hash) → 전송 (원본 비밀번호 안 보냄)
핵심 개념
1. 클라이언트 → 서버 "이 리소스 줘"
2. 서버 → 클라이언트 401 + WWW-Authenticate (realm, nonce 포함)
3. 클라이언트 nonce + realm + PW로 해시값 계산 → Authorization 헤더에 담아 전송
4. 서버 똑같은 방식으로 해시값 계산 → 일치하면 200 OK
Hash 계산 방식:
HA1 = MD5(email:realm:password)
HA2 = MD5(HTTP메소드:URI)
최종 response = MD5(HA1:nonce:HA2)
보안이 중요한 곳에는 SHA-256 이상 권장. MD5는 취약하다.
문제: 매번 요청마다 서버가 난수 생성, 기억, 해시 계산하는 복잡한 과정이 여전히 존재한다.

"이 token을 소지한 요청은 인증, 인가 된 걸로 한다"
Bearer는 소지자, 운반인이라는 뜻이다. 토큰을 들고 있는 사람은 정당한 권한이 있는 걸로 간주한다.
GET /protected/resource HTTP/1.1
Authorization: Bearer mF_9.B5f-4.1JqM
현대 웹에서 가장 많이 사용하는 이유는 ID, PW가 최초 로그인 시 한 번만 전송되기 때문이다. 이후 클라이언트는 token만 보낸다.
장점
| 방식 | 브라우저 | 모바일 앱 | 다른 도메인 |
|---|---|---|---|
| Session + Cookie | ✅ | ❌ | ❌ |
| Bearer Token | ✅ | ✅ | ✅ |
쿠키는 브라우저 전용이고 도메인이 다르면 안 붙지만, 토큰은 헤더에 직접 담아서 보내면 어디서든 동작한다.
CSRF(Cross-Site Request Forgery)는 다른 사이트에서 요청을 위조하는 공격이다. 쿠키는 브라우저가 자동으로 붙이기 때문에 공격자 사이트에서도 쿠키가 첨부된 요청이 날아갈 수 있다. Bearer Token은 JS가 직접 헤더에 담아야 하므로 이 공격이 유효하지 않다.
단점
| 저장소 | 취약점 | 이유 |
|---|---|---|
| localStorage | XSS 공격 | JS로 직접 접근 가능 |
| Cookie | CSRF 공격 | 브라우저가 자동으로 붙임 |
일반 Bearer Token의 한계
매 요청마다 서버가 DB를 조회해서 토큰을 검증해야 한다. DB I/O = 서버 자원 낭비.
| 기존 방식 | 개선 방향 |
|---|---|
| 토큰 = 그냥 열쇠 번호 | 토큰 = 정보가 담긴 문서 |
| 서버가 DB에서 열쇠 확인 | 서버가 문서 자체를 해독·검증 |
| DB I/O 발생 | DB I/O 없음 |
→ JWT
인증 방식 비교
| Basic | Digest | Bearer Token | |
|---|---|---|---|
| 전송 정보 | ID/PW (Base64) | 해시값 | 토큰 문자열 |
| PW 노출 | 사실상 노출 | ❌ | ❌ 전송 안함 |
| 서버 DB 조회 | 매 요청 | 매 요청 | 매 요청 (일반 방식) |
| 재전송 공격 방어 | ❌ | ✅ (nonce) | 유효기간으로 제한 |
| 현재 사용 | 일부 내부 API | 거의 안 씀 | 주류 |
| 한계 | 보안 매우 취약 | 복잡, MD5 취약 | DB 부하 → JWT로 해결 |
애플리케이션 간의 정보 교환을 위해 설계된, 정보를 JSON 형식으로 포함하는 자체 포함형(self-contained) 토큰이다.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJlbWFpbCI6InZvaWRAbWFpbC5jb20iLCJ1c2VybmFtZSI6InZvaWQifQ
.
zKQlHcTw8MZV8Qs4GXj_TQbsFNHqUb6YsIbjxjvmWtw
HEADER . PAYLOAD . SIGNATURE
Header
{
"alg": "HS256",
"typ": "JWT"
}
Payload
{
"email": "void@mail.com",
"username": "void",
"iat": 1609239023,
"exp": 1609242623
}
| 필드 | 의미 |
|---|---|
| iat (Issued At) | 토큰 발급 시간 |
| exp (Expiration) | 토큰 만료 시간 |
⚠️ Payload는 평문 영역이다. 비밀번호 같은 민감 정보는 절대 넣으면 안 된다.
Signature (서명)
"이 토큰이 위조되지 않았다"를 수학적으로 보장하는 부분이다.
Base64(헤더) + Base64(페이로드) + 서버만 아는 비밀키
→ 지정된 알고리즘으로 암호화
내용이 조금이라도 수정되면 signature가 달라진다. 이를 통해 위조를 감지한다.
| 이유 | 설명 |
|---|---|
| 무상태(Stateless) | 서버가 세션 저장 안 해도 됨 |
| DB 부하 없음 | 토큰 자체를 검증하면 끝 (CPU로 처리) |
| 확장성 | 서버가 여러 대여도 같은 비밀키로 검증 가능 |
| 범용성 | 모바일, 웹, 다른 도메인 모두 동작 |
단점
JWT는 한 번 발급되면 서버가 강제 무효화할 수 없기 때문에 토큰을 두 개로 나눠서 사용한다.
| 구분 | Access Token | Refresh Token |
|---|---|---|
| 역할 | API 리소스 접근 출입증 | Access Token 재발급용 인증서 |
| 수명 | 15분 ~ 30분 | 2주 ~ 1달 |
| 저장 위치 | 메모리 또는 LocalStorage 또는 HttpOnly 쿠키 | Redis(서버) 또는 HttpOnly 쿠키 |
| 전송 빈도 | 매 API 요청마다 | Access Token 만료 시에만 |
① 로그인
클라이언트 → 서버: ID/PW
서버 → 클라이언트: Access Token(15분) + Refresh Token(2주)
② 일반 API 요청
Authorization: Bearer <Access Token>
③ Access Token 만료 시
클라이언트 → 서버: Refresh Token으로 재발급 요청
서버 → 클라이언트: 새 Access Token 발급
(유저는 끊김 없이 사용)
Refresh Token이 탈취당하면 Access Token을 오랫동안 발급할 수 있다. JWT 구조 자체가 서버 조회를 없앴기 때문에 주인 판단도 불가하다. → RTR로 해결
"Refresh Token은 한 번 쓰면 즉시 폐기하고, 새 걸로 교체하자"
정상 흐름:
① Access Token 만료
② 유저가 Refresh Token(R1)으로 재발급 요청
③ 서버: Redis에서 R1 확인 → R1 폐기
④ 서버: 새 Access Token + 새 Refresh Token(R2) 발급
⑤ Redis에 R2 저장
탈취 감지:
① 해커가 R1으로 재발급 요청 → 서버: R1 폐기, A2/R2 발급 → 해커에게 전달
② 진짜 유저가 R1으로 재발급 요청
③ 서버: Redis에 R1 없음 → "이미 사용된 토큰 = 탈취 의심"
④ 서버: 이 유저 관련 모든 Refresh Token 전부 만료
⑤ 해커가 들고 있던 R2도 무효화 → 공격 종료
R1이 두 번 사용되는 순간 서버가 이상 감지 → 전체 세션 강제 종료
| 취약점 | 해결책 |
|---|---|
| Payload 내용 노출 | Base64는 암호화 아님 → 민감 정보 절대 넣지 말 것 |
| 토큰 탈취 | HTTPS 필수 + 짧은 유효기간 + HttpOnly 쿠키 |
| 강제 만료 불가 | 토큰 블랙리스트 or 세션 혼합 사용 |
| Refresh Token 탈취 | RTR 적용 |
| 동시 접속 제한 | Refresh Token을 서버(Redis)에서 관리 |
| 토큰 크기 | Payload 최소화 |
현업에서 "완벽한 JWT 단독 사용"은 거의 없다. 서비스 보안 요구수준에 따라 Redis, 세션, RTR 등을 조합해서 쓴다.

"시스템을 책임과 변경 이유에 따라 여러 계층으로 분리해, 각 계층이 정해진 역할만 수행하도록 구성하는 구조"
그냥 순서대로 코드를 쭉 쓰면 SRP 원칙 위반, 중복 코드 증가, 확장성 부족, 유지보수 어려움이 발생한다.
| 계층 | 역할 | 알아야 하는 것 | 몰라도 되는 것 |
|---|---|---|---|
| Controller | HTTP 요청 수신, Service에 위임, 응답 형식 결정 | HTTP, 요청 파라미터 | DB 구조 |
| Service | 비즈니스 규칙 처리, 작업 순서와 흐름 결정 | 비즈니스 로직 | HTTP, DB 구조 |
| Repository | 데이터 조회/저장 | 데이터 저장 방식 | 비즈니스 규칙, HTTP |
Java(객체 지향) ↔ DB(관계 지향) → 근본적인 구조 차이 존재
→ RDB 테이블 구조를 객체화한 것이 Entity (테이블 정의, column mapping, key 정의)
MVC 패턴 vs Layered Architecture
| MVC 패턴 | Layered Architecture | |
|---|---|---|
| 관점 | 요청이 어떻게 흘러가는가 | 코드를 어떤 기준으로 나누는가 |
| 초점 | 요청 처리 흐름 | 책임 분리 |
실무에서는 MVC 흐름 위에 Layered Architecture를 얹어서 사용한다.
계층마다 필요한 데이터가 다르기 때문에 계층 간 전달할 데이터 구조를 정의한 것이다.
// Entity: 모든 정보 포함
public class User {
private Long id;
private String email;
private String password; // 외부에 노출하면 안 됨
private String username;
}
// DTO: 외부에 줄 데이터만
public class UserResponseDto {
private Long id;
private String email;
private String username;
// password 제외
}
Entity ↔ DTO 변환 로직을 한 곳에 모아둔 클래스다.
계층 간 데이터가 전달될 때 이를 가로채서 처리하는 역할이다.
클라이언트 요청
↓
[Filter] ← Spring 밖 (서블릿 레벨)
↓
[Interceptor] ← Spring 안, Controller 도달 전
↓
Controller
| Filter | Interceptor | |
|---|---|---|
| 위치 | Spring 밖 (서블릿 레벨) | Spring 안 |
| 적용 대상 | 모든 요청 | Spring이 처리하는 요청만 |
| Spring Bean 사용 | 어려움 | 가능 |
| 주요 사용처 | CORS, 인코딩, 보안 | JWT 인증, 로깅, 권한 확인 |
DTO @Valid
클라이언트가 잘못된 데이터를 보내는 경우를 DTO에서 검증 규칙으로 막는다.
public class SignupRequestDto {
@NotBlank @Email
private String email;
@NotBlank @Size(min = 8)
private String password;
}
// Controller에서 @Valid 적용
@PostMapping("/signup")
public ResponseEntity<?> signup(@RequestBody @Valid SignupRequestDto dto) {
// 여기 도달 = 검증 통과한 데이터
}
전체 요청 흐름
클라이언트 요청
↓
[Filter] CORS, 인코딩 처리
↓
[Interceptor] JWT 토큰 검증 → 실패 시 401 차단
↓
Controller @Valid로 DTO 검증 → 실패 시 400 차단
↓
[Mapper] DTO → Entity 변환
↓
Service 비즈니스 로직
↓
Repository DB 접근
↓
[Mapper] Entity → DTO 변환
↓
클라이언트 응답