JWT 방식 로그인을 선택하는 이유
1. 세션 기반 인증
JWT에 대해 알아보기 전에, 전통적으로 널리 사용되어 오고있는 세션 기반 인증 방식에 대해 알아보도록(까보도록) 하겠습니다.
세션 기반 인증은 로그인 후 서버에서 세션 ID를 생성해 서버 메모리에 저장합니다.
클라이언트는 쿠키에 응답받은 세션 ID를 저장합니다.
이후 요청마다 자동으로 쿠키를 전송하여 로그인한 사용자인지 식별합니다.
장점
- 구현이 비교적 쉬움
- 로그아웃, 강제 만료 등 로그인 상태를 서버에서 직접 관리
- 최초 쿠키만 세팅하면 클라이언트에서 신경쓸게 없음
단점
- 모든 로그인한 사용자의 상태를 서버 메모리에 저장
- 서버가 여러개인 환경에서는 세션을 공유해야 하는 문제 발생
- 쿠키 탈취 위험
- 웹브라우저에 의존적
2. JWT 방식
JWT 방식에서는 서버가 로그인 상태를 저장하지 않고, 클라이언트에서 토큰을 보관합니다.
서버에서는 토큰의 유효성만 검증하면 되기 때문에, 확장성 측면에서 유리합니다.
JWT란?
JSON 형태의 정보가 인코딩 된 토큰입니다.
구성은 다음과 같습니다.
{Header}.{Payload}.{Signature}
예시 :
{"alg": "HS256", "typ": "JWT"}.{"id": "1", "role": "ADMIN", "exp": 1730609000}.{signature}
Header: 토큰을 파싱하고 검증하기 위한 정보
Payload: 발급 대상, 권한, 유효시간 및 커스텀 데이터 등
Signature: 우리 서버에서 발급한게 맞는지 식별하기 위한 서명
실습을 진행하며 토큰이 정말 이렇게 생겼는지 같이 확인해 보겠습니다.
로그인
이제부터 로그인을 구현해 보겠습니다.
우선 로그인은 "인증"과 "인가"로 나뉩니다.
인증(Authentication)은 "누구세요?", 인가(Authorization)는 "너 뭐 돼?" 라고 보면 됩니다.
호텔 투숙객으로 예시를 들어보겠습니다.
카운터에서 호텔에 예약한 사람이 신분증을 제시하고 예약한 사람이 맞는지 확인하고 객실키를 받는 것이 인증입니다.
이 사람이 자기가 예약한 505호 방으로 키를 찍고 들어가는 것이 인가입니다. 504호에 키를 찍으면 권한이 없어서 들어갈 수 없겠죠?
인증
이제 로그인 중 "인증"을 먼저 구현해 보도록 하겠습니다.
시스템에서 인증 과정은 다음과 같습니다.
1. ID/PW를 통해 로그인 요청
2. ID/PW 검증 및 회원 식별
3. 토큰 발급
+ 클라이언트는 토큰을 스토리지에 저장 (클라이언트 구현은 실습에 없기 때문에, 저는 메모장에 저장하겠읍니다.)
AuthController.java
@RestController
@RequiredArgsConstructor
@RequestMapping("/auth")
public class AuthController {
private final AuthService authService;
@PostMapping
public ResponseEntity<AuthDto.LoginResponse> login(@RequestBody AuthDto.LoginRequest request) {
return ResponseEntity.ok(authService.login(request));
}
}
AuthDto.java
public class AuthDto {
@Getter @Builder
public static class LoginRequest {
private String userId;
private String userPw;
}
@Getter @Builder
public static class LoginResponse {
private String refreshToken;
private String accessToken;
}
}
AuthService.java
@Service
@RequiredArgsConstructor
public class AuthService {
private String secretKey = "jaeng";
private final static Long ACCESS_TOKEN_VALIDITY = 30 * 60 * 1000L; // 30분
private final static Long REFRESH_TOKEN_VALIDITY = 12 * 60 * 60 * 1000L; // 12시간
private final UserRepository userRepository;
@Transactional
public AuthDto.LoginResponse login(AuthDto.LoginRequest request) {
User user = userRepository.findByUserId(request.getUserId()).orElseThrow(() -> loginFailException());
if (!Objects.equals(user.getUserPw(), request.getUserPw())) {
throw loginFailException();
}
return AuthDto.LoginResponse.builder()
.accessToken(createToken(user.getId(), ACCESS_TOKEN_VALIDITY))
.refreshToken(createToken(user.getId(), REFRESH_TOKEN_VALIDITY))
.build();
}
private String createToken(Long sub, Long expiresIn) {
Claims claims = Jwts.claims();
claims.put("sub", String.valueOf(sub)); // 커스텀 데이터
Date now = new Date();
return Jwts.builder()
.setHeaderParam(Header.TYPE, Header.JWT_TYPE) // 토큰 헤더 정보
.setClaims(claims) // 페이로드
.setIssuedAt(now) // 발급시간
.setExpiration(new Date(now.getTime() + expiresIn)) // 만료시간
.signWith(SignatureAlgorithm.HS256, secretKey) // 서명
.compact();
}
private SecurityException loginFailException() {
return new SecurityException("로그인 정보가 잘못되었습니다.");
}
}
토큰에는 다음과 같은 정보가 포함됩니다.
1. Header : 토큰 타입 정보, 서명 알고리즘 정보 => { "typ": "JWT", "alg": "HS256" } 형태로 들어갑니다.
2. Payload : claims로 세팅한 데이터, 발급시각, 만료시각 => { "sub": "1", "iat": "..", "exp": ".." }, 인가에서 필요한 정보를 추가할 수 있습니다.
3. Signature : 시크릿키 값
결과 확인

회원 정보를 올바르게 입력하면 이렇게 토큰 정보를 응답받습니다.
토큰 내용은 jwt.io 에서 확인할 수 있습니다.

createToken에서 생성한 내용이 잘 들어 가있군요 후후.
여기까지는 로그인 중 인증 과정을 알아보았습니다.
다음편에서 본격적인 Spring Security 구성과 인가를 구현해 보겠습니다.
'Server > SpringBoot' 카테고리의 다른 글
| [SpringBoot] 조회 API 만들기 (1) | 2025.04.26 |
|---|---|
| [SpringBoot] 레이어드 아키텍처를 적용하여 API 만들기 (1) | 2025.04.26 |
| [SpringBoot] JPA, MySQL 설정 (0) | 2025.04.26 |
| [SpringBoot] 프로젝트 만들기 (1) | 2025.04.26 |