프로그래밍 개념 정리

jwt, 엑세스 토큰, 리프레시 토큰

Dr.Mlem 2025. 4. 13. 20:20
Q1.
jwt를 작성하다보니 개념이 잘 이해가 안돼. 리프레시 토큰? 엑세스 토큰? 이런건 뭐하는 개념이야?
 
 
 
A1.
좋은 질문이야! JWT를 처음 접할 때 가장 헷갈리는 개념 중 하나가 바로 **Access Token(액세스 토큰)**과 **Refresh Token(리프레시 토큰)**이야. 개념을 간단하고 명확하게 정리해줄게.

🔐 JWT (JSON Web Token)

JWT는 인증 정보를 담은 디지털 토큰이야. 클라이언트가 서버에 로그인 요청을 하면, 서버는 JWT를 만들어서 클라이언트에 전달하고, 클라이언트는 그걸 저장해서 이후 요청에 첨부해. 서버는 이 토큰을 보고 "이 사용자가 인증된 사용자구나"를 확인해.


🧾 Access Token (액세스 토큰)

  • 사용 목적: 인증된 사용자가 API 요청을 할 때 자격 증명으로 사용.
  • 특징:
    • **수명(Time-to-Live)**이 짧아 (보통 몇 분 ~ 1시간).
    • 탈취되더라도 위험이 제한됨 (짧은 시간만 유효하니까).
    • 클라이언트는 서버에 요청할 때 이 토큰을 Authorization 헤더에 담아서 보냄.
Authorization: Bearer <ACCESS_TOKEN>

♻️ Refresh Token (리프레시 토큰)

  • 사용 목적: Access Token이 만료됐을 때, 새로운 Access Token을 발급받기 위한 용도.
  • 특징:
    • 보통 수명이 깁니다 (며칠 ~ 몇 주).
    • 서버 또는 안전한 저장소(예: HTTPOnly 쿠키)에 저장함.
    • Refresh Token 자체로는 API 호출이 안 됨. 오직 Access Token 갱신용으로만 사용됨.

💡 왜 둘로 나눌까?

Access Token을 짧게 가져가고, Refresh Token을 따로 쓰는 이유는 보안 때문이야.

  • Access Token만 쓰면 유출됐을 때 큰 피해를 볼 수 있음.
  • Access Token을 짧게 하고, Refresh Token으로만 갱신하게 하면 유출 위험을 최소화할 수 있어.
  • 예를 들어: Access Token이 탈취되면 금방 만료돼서 못 쓰고, Refresh Token은 안전한 곳에만 저장돼서 보호됨.

🔁 작동 흐름 요약

  1. 사용자가 로그인 → Access Token + Refresh Token 발급
  2. 사용자가 API 요청할 때 → Access Token 사용
  3. Access Token 만료 → Refresh Token으로 새 Access Token 요청
  4. Refresh Token도 만료 → 다시 로그인 필요

필요하면 그림으로도 정리해줄 수 있어!
혹시 JWT로 구현 중인 구조나 코드가 있다면 같이 봐줘도 설명이 더 명확해질 수 있어.

 

 

 

Q2.

그림으로도 그려주고,

func GenerateJWT(id, name string) (string, error) {
	log.Println("[DEBUG] JWT 작성 시작")
	
	jti := uuid.NewString()                    // 고유한 토큰 ID 생성
	secret := uuid.NewString()                // 랜덤한 서명키 생성
	expiration := time.Hour * 1               // 1시간 유효

	// Redis에 jti ->  secret 저장
	err := redisdb.RDB.Set(redisdb.Ctx, jti, secret, expiration).Err()
	if err != nil {
		return "", fmt.Errorf("redis 저장 실패: %v", err) 
	}
	log.Println("[DEBUG] JWT 서명 작성 완료")
	
	// jwt에 담을 사용자 정보와 메타데이터
	claims := jwt.MapClaims{
		"id":   id,
		"name": name,
		"jti":  jti,
		"exp":  time.Now().Add(expiration).Unix(),
	}

	// JWT 생성 HS256 알고리즘
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

	log.Println("[DEBUG] JWT 작성 완료")

	// 토큰을 비밀키로 서명해서 문자열 반환
	return token.SignedString([]byte(secret))
}

// JWT 유효성 검증 함수
// JWT의 jti를 추출하고, redis에서 서명 키를 가져와 유효성 검사 수행
func ValidateJWT(tokenStr string) (jwt.MapClaims, error) {
	
	log.Println("[DEBUG] JWT 검증 시작")

	// jwt에서 먼저 jti 클레임만 파싱(서명 검증 없이 파싱)
	parser := jwt.Parser{}
	claims := jwt.MapClaims{}
	_, _, err := parser.ParseUnverified(tokenStr, claims)
	if err != nil {
		return nil, fmt.Errorf("JWT 파싱 실패: %v", err)
	}

	// jti 클레임 추출
	jti, ok := claims["jti"].(string)
	if !ok {
		return nil, fmt.Errorf("jti 클레임 누락됨")
	}
	log.Println("[DEBUG] jti 파싱 완료")

	// Redis에서 jti에 해당하는 secret 조회
	secret, err := redisdb.RDB.Get(redisdb.Ctx, jti).Result()
	if err != nil {
		return nil, fmt.Errorf("redis에서 Secret 조회 실패: %v", err)
	}
	log.Println("[DEBUG] redis에서 Secret 조회 완료")

	// 실제 JWT 검증 (서명 포함)
	token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
		return []byte(secret), nil
	})
	if err != nil || !token.Valid {
		return nil, fmt.Errorf("유효하지 않은 토큰")
	}

	// 클레임 최종 추출
	finalClaims, ok := token.Claims.(jwt.MapClaims)
	if !ok {
		return nil, fmt.Errorf("클레임 파싱 실패")
	}
	
	log.Println("[DEBUG] JWT 검증 완료")
	
	return finalClaims, nil
}

// HTTP 요청의 Authorization 헤더에서 JWT를 추출하고, 유효성 검사까지 수행
func AuthorizeJWT(ctx *fasthttp.RequestCtx) (jwt.MapClaims, error) {

	// Authorization 헤더에서 jwt 추출
	auth := string(ctx.Request.Header.Peek("Authorization"))
	if !strings.HasPrefix(auth, "Bearer ") {
		return nil, fmt.Errorf("'Authorization' 헤더 없음")
	}
	log.Println("[DEBUG] JWT 인증 시작")

	// Berarer 접두사 제거
	tokenStr := strings.TrimPrefix(auth, "Bearer ")

	// JWT 유효성 검사
	claims, err := ValidateJWT(tokenStr)
	if err != nil {
		return nil, fmt.Errorf("JWT 검증 실패: %v", err)
	}

	log.Println("[DEBUG] JWT 인증 성공")
	return claims, nil
}

 

이 코드에서 사용하는 토큰은 어떤 토큰인거야?

 

 

 

A2.

아주 좋아! 지금 제공해준 코드를 보면, **GenerateJWT에서 생성하는 토큰은 "Access Token"**이야.
그리고 현재 구조상 "Refresh Token"은 따로 존재하지 않아.


✅ 지금 코드 구조에서 사용하는 토큰의 정체

🟢 Access Token (✔ 사용 중)

  • GenerateJWT(id, name) 함수가 생성하는 JWT는 사용자의 인증 정보를 담고 있고,
  • **1시간 유효한 만료 시간(exp)**을 가지고 있음
  • JWT 안에 들어있는 jti 값을 기준으로 Redis에 서명키를 저장하고,
  • 이후 검증 시 이 jti로 Redis에서 secret을 다시 조회해서 서명을 검증함

👉 즉, 이건 Access Token의 역할을 하는 거야.

🔴 Refresh Token (✖ 현재 없음)

  • Refresh Token은 Access Token이 만료됐을 때 새 Access Token을 발급받기 위한 용도인데,
  • 너의 코드에서는 그런 용도의 별도 토큰이 존재하지 않아.

👉 만약 Access Token이 만료되면 지금은 다시 로그인하거나 새로 발급받아야 해.


🔁 Access / Refresh Token 도식

너의 이해를 돕기 위해 아래에 Access Token / Refresh Token 구조와 흐름을 정리한 그림을 만들어봤어:

🔽 Access Token & Refresh Token 흐름

[로그인 요청]
     ↓
[서버에서 사용자 인증 성공]
     ↓
[Access Token + Refresh Token 발급]
     ↓
[Access Token → 클라이언트 저장 (ex. 메모리)]
[Refresh Token → 보안 저장소 (ex. HTTPOnly 쿠키)]
     ↓
[API 요청 시 Access Token 첨부]
     ↓
[Access Token 만료됨]
     ↓
[Refresh Token으로 새로운 Access Token 요청]
     ↓
[새로운 Access Token 발급]

 

✅ JWT 인증 흐름 (Access Token + Refresh Token)

1️⃣ 클라이언트 → 서버 : 로그인 요청

설명: 사용자가 아이디와 비밀번호 등으로 로그인 요청을 보냅니다.

2️⃣ 서버 → 클라이언트 : Access Token과 Refresh Token 발급

설명: 서버는 인증 정보를 확인한 뒤, Access Token과 Refresh Token을 생성하여 클라이언트에 전달합니다.

3️⃣ 클라이언트 → 서버 : API 요청 시 Access Token 포함

설명: 클라이언트는 이후 API 요청을 보낼 때 Access Token을 HTTP 헤더에 포함하여 보냅니다.

4️⃣ 서버 : Access Token의 유효성 검증

설명: 서버는 전달받은 Access Token을 검증하여 사용자의 권한을 확인합니다.

5️⃣ Access Token 만료 시 → 클라이언트 → 서버 : Refresh Token으로 재요청

설명: Access Token이 만료된 경우, 클라이언트는 보관 중인 Refresh Token을 이용하여 새 Access Token을 요청합니다.

6️⃣ 서버 : Refresh Token 검증 후 새로운 Access Token 발급

설명: 서버는 Refresh Token이 유효하다면, 새로운 Access Token을 생성하여 클라이언트에 전달합니다.

'프로그래밍 개념 정리' 카테고리의 다른 글

Gaia-X는 Data Space가 아니다  (0) 2025.04.22
EDC 핵심 기능  (0) 2025.04.22
임시 JWT 비교  (0) 2025.04.11
상수와 리터럴 차이  (0) 2025.04.04
라이브러리와 패키지, 핸들러  (0) 2025.04.04