애플리케이션 개발 중, 인증과 인가를 구현하기 위해 JWT를 다시 공부하기로 했다. 프로젝트 때 한 번 다뤄봤지만, 내가 직접 구현한 게 아니었기에 당시에는 어려워 보였다. 다행히 이번에는 JWT를 직접 구현하면서, 어느 정도 익숙해졌다. 현재 프로젝트에서도 JWT 구현이 잘 마무리되었고, 사이드 프로젝트에서도 JWT 관련 부분을 맡기로 했기에, 머지않아 곧 JWT 전문가가 될 것 같다! JWT는 마주칠때마다 생각하게 만든다. 바로 탁 ! 안나온다. 그래서 벨로그에 포스팅하면서 한번 더 제대로 정리해보려 한다 !!!
JWT (JSON Web Token)는 오늘날 RESTful API를 개발할 때, 자주 사용되는 인증 방식이다. JWT는 암호화된 JSON 객체로, 사용자 정보와 같은 중요한 데이터를 서버와 클라이언트 간에 주고받을 때 사용된다. 이번 포스팅에서는 JWT를 활용한 인증 시스템을 구축하고, 인증 서버에서 액세스 토큰과 리프레시 토큰을 발급하여 사용하는 방식부터, API 게이트웨이에서 토큰을 검증하고 라우팅하는 과정, 그리고 토큰을 갱신하는 방법까지 다뤄보려한다 !!!
JWT는 Header, Payload, Signature 세 가지 요소로 구성된 문자열
{
"alg": "HS256",
"typ": "JWT"
}
Base64로 인코딩되어 저장된다. 아래는 예시다.{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
→ 쉽게 말하자면,
Signature = Header + Payload를 비밀키(Secret Key)를 통해 해싱 한 값
JWT Token = Header + Payload + Signature
이다 !!!!!!
JWT 인증 시스템은 인증 서버, API 게이트웨이, 서비스 서버 세 가지 구성 요소로 나뉜다.
// 로그인 시 응답 예시
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

플로우를 이해하는게 너무나 헷갈려서 내가 공부하면서 직접 그린 토큰 발급 플로우다 ! ! !
Authorization 헤더에 액세스 토큰을 포함하여 요청GET /api/user/profile
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Authorization 헤더에 포함된 토큰을 검증JWT의 단점 중 하나는 액세스 토큰의 유효 기간이 짧다는 점이다. 그래서 액세스 토큰이 만료되었을 때 리프레시 토큰을 사용해 새로운 액세스 토큰을 발급받아야 한다.
POST /api/auth/refresh
Authorization: Bearer 리프레시 토큰
Authorization 헤더에 포함해 다시 요청문득 생각이 들었다. 리프레시 토큰은 수명이 긴데, 리프레시 토큰이 탈취당하면 너무 위협적이지 않아 ?!@!#!?
→ 그래서 리프레시 토큰으로 액세스 토큰을 갱신할때 주로 액세스 토큰만 갱신하지않고 리프레시 토큰도 같이 갱신한다고한다.
실제 자바 코드를 통해 인증 시스템을 학습해보자 !
public class JwtTokenProvider {
private String secretKey = "mySecretKey"; // 시크릿 키 (임의의 문자열)
// 액세스 토큰 생성
public String createAccessToken(String userId) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + 1000 * 60 * 10); // 10분 유효기간
return Jwts.builder()
.setSubject(userId) // 사용자 ID 설정
.setIssuedAt(new Date()) // 발행 시간
.setExpiration(expiryDate) // 만료 시간
.signWith(SignatureAlgorithm.HS512, secretKey) // 시크릿 키와 함께 서명
.compact();
}
// 리프레시 토큰 생성
public String createRefreshToken(String userId) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + 1000 * 60 * 60 * 24 * 7); // 7일 유효기간
return Jwts.builder()
.setSubject(userId)
.setIssuedAt(new Date())
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, secretKey)
.compact();
}
// 토큰 검증
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
return true;
} catch (Exception e) {
return false; // 토큰이 유효하지 않을 경우 false 반환
}
}
// 토큰에서 사용자 정보 추출
public String getUserIdFromToken(String token) {
Claims claims = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token)
.getBody();
return claims.getSubject();
}
}
설명:
createAccessToken 메서드는 주어진 사용자 ID를 기반으로 액세스 토큰을 생성
createRefreshToken 메서드는 더 긴 유효 기간을 가진 리프레시 토큰을 생성
validateToken 메서드는 토큰의 유효성을 검사하고, 유효하지 않은 경우 false를 반환
getUserIdFromToken 메서드는 토큰에서 사용자 ID를 추출
→ 주로 토큰에서 사용자의 데이터를 추출하는 메서드는 게이트웨이에서 사용한다.
이번 포스팅에서는 JWT를 활용한 인증 시스템의 전체적인 흐름을 살펴보았다. 인증 서버에서 액세스 토큰과 리프레시 토큰을 발급하고, API 게이트웨이에서 토큰을 검증한 후 서비스 서버로 요청을 전달하는 방식으로 시스템을 구축했다. JWT를 통해 인증을 구현하면 클라이언트와 서버 간의 통신을 효율적으로 관리할 수 있으며, 이를 통해 개발자는 더 안전하고 일관된 인증 시스템을 제공할 수 있다.
그리고 !! 이전 프로젝트에서는 토큰에서 사용자 데이터를 추출하는것을 각 비즈니스 서버에서 수행했는데 게이트웨이에서 일괄처리해서 적절히 라우팅해주니 중복코드도 줄어들고 비즈니스 로직에 더 집중할수있고 유지보수도 용이해졌다 !