로그인 기능을 개발하면서 사용자 인증의 핵심 요소인 JWT(JSON Web Token)를 본격적으로 도입하게 되었다. 기존의 세션 방식에서 벗어나 더욱 안전하고 확장성 있는 인증 메커니즘을 구현하고 싶었다. 이 글에서는 내가 실제 프로젝트에서 JWT를 어떻게 구현하고 활용했는지 상세히 공유하려 한다.
JWT란?
당사자 간에 정보를 JSON 객체로 안전하게 전송하기 위한 간결하고 자가수용적인 방식이다. 주로 인증과 정보 교환에 사용되며, 세 부분으로 구성된다.
- Header(헤더)
- 토큰의 유형과 해시 알고리즘 정보를 포함한다.
- 예: 사용된 해시 알고리즘(HMAC SHA256), 토큰 타입(JWT)
- Payload(페이로드)
- 토큰에 담길 정보(클레임)를 포함한다.
- 사용자 ID, 이름, 만료 시간 등 다양한 데이터 저장 가능
- Signature(서명)
- 토큰의 무결성을 보장하는 부분이다.
- 헤더, 페이로드, 비밀 키를 사용하여 생성
JWT를 사용하는 이유
- 상태 비저장(Stateless) 인증
- 서버가 세션을 저장하지 않고 토큰만으로 인증 가능
- 서버 확장성과 마이크로서비스 아키텍처에 적합
- 보안성
- 디지털 서명으로 데이터 변조 방지
- 토큰 자체에 만료 시간 포함으로 보안 강화
- 크로스 도메인 인증
- 모바일 앱, 웹 서비스 등 다양한 플랫폼에서 쉽게 인증 가능
- RESTful API 인증에 이상적
- 확장성
- 추가 정보를 쉽게 토큰에 포함 가능
- 클라이언트와 서버 간 정보 교환에 유연
JWT 사용 예시
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. // 헤더
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ. // 페이로드
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c // 서명
JWT 유틸리티 클래스 구현
JWT 유틸리티 클래스의 주요 구성
public class JwtUtil {
// 비밀 키와 만료 시간 설정
private static final String SECRET_KEY = "1234"; // 실제 환경에서는 보다 복잡한 키 사용 권장
private static final long EXPIRE_TIME = 1000 * 60 * 60; // 1시간 유효 시간
}
JWT 구현의 첫 단계로 비밀 키와 토큰 만료 시간을 설정했다. 비밀 키는 토큰 서명에 사용되며, 만료 시간은 보안을 위해 1시간으로 제한했다.
토큰 생성 메서드
private static String createToken(String tokenSuject) {
return JWT.create()
.withSubject("" + tokenSuject) // 토큰의 주체 (사용자 번호)
.withExpiresAt(new Date(System.currentTimeMillis() + EXPIRE_TIME)) // 만료 시간 설정
.sign(Algorithm.HMAC512(SECRET_KEY)); // HMAC512 알고리즘으로 서명
}
토큰 생성 메서드에서는 다음과 같은 중요한 작업을 수행한다.
- 토큰의 주체(Subject)로 사용자 번호 설정
- 현재 시간 기준 1시간 후를 만료 시간으로 지정
- HMAC512 알고리즘을 사용하여 비밀 키로 토큰 서명
토큰 검증 및 추출 메서드
public static boolean checkToken(String token) {
try {
JWTVerifier verifier = JWT.require(Algorithm.HMAC512(SECRET_KEY)).build();
verifier.verify(token); // 토큰 검증
System.out.println("토큰이 유효합니다.");
return true;
} catch (JWTVerificationException exception) {
System.out.println("토큰이 유효하지 않습니다.");
return false;
}
}
토큰 검증 메서드는 다음과 같은 기능을 수행한다:
- 동일한 비밀 키로 토큰 검증
- 토큰의 유효성, 만료 여부 확인
- 유효하지 않은 경우 예외 처리
로그인 컨트롤러에서의 JWT 토큰 활용
일반 로그인 시 토큰 발급
@PostMapping("/api/users/login")
public JsonResult login(@RequestBody JuLoginVo juLoginVo, HttpServletResponse response) {
JuLoginVo authUser = juUserService.exeLogin(juLoginVo);
if (authUser != null) {
// 업체 계정 활성화 검증
if ("업체".equals(authUser.getType()) && !authUser.isIs_active()) {
return JsonResult.fail("승인되지 않은 계정입니다. 관리자에게 문의하세요.");
}
// JWT 토큰 생성 및 응답 헤더 설정
JwtUtil.createTokenAndSetHeader(response, "" + authUser.getMember_id());
return JsonResult.success(authUser);
} else {
return JsonResult.fail("아이디 또는 비밀번호가 잘못되었습니다.");
}
}
로그인 과정에서 다음과 같은 JWT 토큰 발급 절차를 구현했다.
- 로그인 성공 시 사용자 ID로 JWT 토큰 생성
- 응답 헤더의 Authorization에 "Bearer {토큰}" 형식으로 토큰 추가
- 추가 보안을 위해 업체 계정의 경우 활성화 상태 확인
소셜 로그인 시 토큰 발급
@PostMapping("/api/users/social/signup")
public JsonResult socialSignUp(@RequestBody JuUserVo juUserVo, HttpServletResponse response) {
try {
// 소셜 회원가입 처리
int count = juUserService.exeSocialSignUp(juUserVo);
if (count != 1) {
return JsonResult.fail("회원등록에 실패했습니다.");
}
// 회원 정보 조회
JuLoginVo authUser = juUserService.exeSocialLogin(juUserVo.getEmail());
if (authUser != null) {
// JWT 토큰 생성 및 응답 헤더 설정
JwtUtil.createTokenAndSetHeader(response, "" + authUser.getMember_id());
return JsonResult.success(authUser);
} else {
return JsonResult.fail("회원가입은 성공했으나 사용자 정보를 가져올 수 없습니다.");
}
} catch (Exception e) {
return JsonResult.fail("회원가입 처리 중 오류 발생: " + e.getMessage());
}
}
소셜 로그인에서도 동일한 JWT 토큰 발급 방식을 적용했다.
- 소셜 회원가입 성공 후 회원 정보 조회
- 조회된 사용자 ID로 JWT 토큰 생성
- 응답 헤더에 토큰 추가
마무리
JWT 토큰 구현을 통해 다음과 같은 이점을 얻을 수 있었다.
- 안전하고 확장 가능한 사용자 인증 메커니즘
- 상태 비저장(Stateless) 인증 방식 구현
- 토큰 만료 및 보안 설정을 통한 보안성 강화
개발 과정에서 주의해야 할 점은 실제 운영 환경에서는 더욱 복잡하고 안전한 비밀 키를 사용해야 한다는 것이다.
'SpringBoot' 카테고리의 다른 글
[SpringBoot] sts4 설치 및 압축풀기 오류 해결 (0) | 2025.04.24 |
---|---|
[SpringBoot] 일관된 API 응답을 위한 JsonResult 구현 및 활용 (0) | 2025.03.07 |
[SpringBoot] 스프링부트 어노테이션(Annotation) 정리 (0) | 2025.03.05 |
[SpringBoot]AWS S3를 활용한 파일 업로드 구현하기 (0) | 2025.02.28 |
[SpringBoot] Twilio를 활용한 기념일 알림 기능 개발 (0) | 2025.02.23 |