JWT 소개

JWT(JSON Web Token)는 웹에서의 간결하고 자기 포함 방식의 인증 방법입니다. 헤더, 페이로드 및 시그니처 세 부분으로 구성됩니다. 헤더에는 토큰의 유형 및 암호화 알고리즘 정보, 페이로드에는 전달될 데이터(일반적으로 일부 권한 정보 및 사용자 식별 정보를 포함)가 포함되어 있으며 시그니처는 토큰의 무결성과 유효성을 확인하는 데 사용됩니다.

JWT는 주로 두 가지 시나리오에서 사용됩니다:

  1. 인증: 사용자가 로그인한 후 각 후속 요청에는 JWT가 포함되어 있어 사용자가 허용된 경로, 서비스 및 리소스에 액세스할 수 있습니다.
  2. 정보 교환: JWT는 디지털로 서명될 수 있으므로 당사자 간에 안전하게 정보를 전송하는 데 좋은 방법입니다. 예를 들어 공개/개인 키 쌍을 사용하여 디지털로 서명할 수 있습니다.

JWT 데이터 형식

JWT는 당사자 간에 교환될 정보를 간결하고 URL 안전하게 나타내는 방법입니다. JWT는 사실상 헤더.페이로드.시그니처로 구성된 세 부분으로 나뉩니다. 다음으로, 이 세 부분의 데이터 형식을 자세히 설명하겠습니다.

1. 헤더

헤더는 일반적으로 두 부분으로 구성됩니다: 토큰 유형(일반적으로 JWT) 및 사용된 서명 또는 암호화 알고리즘(예: HMAC SHA256 또는 RSA)입니다. 헤더는 JSON으로 표시한 다음 Base64Url로 인코딩됩니다. 예를 들면:

{
  "alg": "HS256",
  "typ": "JWT"
}

인코딩 후에는 다음과 유사한 문자열을 얻을 수 있습니다:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

2. 페이로드

페이로드에는 일련의 클레임이 포함되어 있으며, 이는 주체(일반적으로 사용자)와 기타 데이터에 대한 정보입니다. 페이로드에는 여러 사전 정의된 클레임(등록된 클레임이라고도 함) 및 사용자 정의 클레임(개인 클레임)이 포함될 수 있습니다.

사전 정의된 클레임은 다음과 같습니다:

  • iss (Issuer): 발급자
  • exp (Expiration Time): 만료 시간
  • sub (Subject): 주제
  • aud (Audience): 청중
  • iat (Issued At): 발급 시간
  • nbf (Not Before): 유효 시간
  • jti (JWT ID): JWT의 고유 식별자

예를 들어, 페이로드는 다음과 같이 보일 수 있습니다 (JSON 형식):

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true,
  "iat": 1516239022
}

이 정보는 또한 Base64Url로 인코딩되며, 다음과 유사한 문자열을 얻을 수 있습니다:

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0

3. 시그니처

시그니처 섹션은 앞에서 언급한 두 인코딩된 문자열을 서명하여 전송 중에 메시지가 변경되지 않았음을 확인하는 데 사용됩니다. 먼저 HMAC SHA256 알고리즘을 사용하는 경우 키를 지정하고, 헤더에 지정된 알고리즘을 사용하여 헤더와 페이로드를 서명해야 합니다.

예를 들어, 다음과 같은 헤더 및 페이로드가 있는 경우:

HeaderEncoded.HeaderPayload

키를 사용하여 서명하기 위한 의사 코드는 다음과 같을 수 있습니다:

HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secretKey)

그 결과로 생성된 서명된 문자열은 다음과 같을 수 있습니다:

dBjftJeZ4CVPmTaoyL4IiArYfL4kH0jOspm6XwbcJXY

완전한 JWT

이 세 부분을 점(.)으로 구분하여 결합하면 완전한 JWT가 형성됩니다:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.dBjftJeZ4CVPmTaoyL4IiArYfL4kH0jOspm6XwbcJXY

JWT 표준은 매우 유연하며 페이로드에 필요한 모든 정보를 저장할 수 있습니다 (JWT 크기를 줄이고 보안상의 이유로 민감한 정보는 저장하면 안 됨). 또한 시그니처를 통해 이 정보의 무결성을 보장합니다.

Go JWT 라이브러리 설치하기

Go 커뮤니티에서는 JWT 처리를 위한 github.com/golang-jwt/jwt 패키지를 제공합니다. 이 라이브러리를 설치하는 것은 매우 간단합니다. 프로젝트 디렉토리에서 다음 명령을 실행하면 됩니다:

go get -u github.com/golang-jwt/jwt/v5

설치가 완료되면 다음과 같이 import 구문에 포함시킬 수 있습니다:

import "github.com/golang-jwt/jwt/v5"

간단한 토큰 생성하기

Go 언어와 HS256 알고리즘을 사용하여 간단한 JWT 토큰을 생성하려면 다음 단계를 따르셔야 합니다:

먼저, HS256 알고리즘을 사용하여 새 Token 객체를 생성합니다:

token := jwt.New(jwt.SigningMethodHS256)

다음으로, SignedString 메서드를 사용하여 이 토큰의 문자열 표현을 생성합니다. 이때 사용할 키를 전달합니다.

var mySigningKey = []byte("your-256-bit-secret")
strToken, err := token.SignedString(mySigningKey)
if err != nil {
    log.Fatalf("오류 발생: %v", err)
}
fmt.Println(strToken)

이렇게 하면 어떠한 클레임도 포함되지 않은 간단한 토큰이 생성됩니다.

매개변수를 포함하는 토큰 생성하기

JWT의 주요 기능 중 하나는 정보를 전달하는 것입니다. 이러한 정보는 클레임을 통해 토큰에 인코딩됩니다. 예를 들어 사용자 정의 클레임을 만들어보겠습니다:

// 사용자 정의 클레임 구조체
type MyClaims struct {
	jwt.RegisteredClaims
	Username string `json:"username"`
	Admin    bool   `json:"admin"`
}

// 사용자 정의 클레임을 포함한 토큰 생성
claims := MyClaims{
    RegisteredClaims: jwt.RegisteredClaims{},
    Username: "my_username",
	Admin: true,
}

token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

// 서명을 위한 비밀 키
var mySigningKey = []byte("your-256-bit-secret")

// 문자열 형식으로 토큰 생성
strToken, err := token.SignedString(mySigningKey)
if err != nil {
    log.Fatalf("오류 발생: %v", err)
}

fmt.Println(strToken)

위 코드에서는 등록된 클레임과 사용자 정의 정보를 포함하는 MyClaims 구조체를 정의했습니다. 그런 다음 여전히 SignedString을 사용하여 토큰 문자열을 생성합니다.

토큰 구문 분석 및 유효성 검사하기

JWT의 구문 분석과 유효성 검사는 매우 중요합니다. 다음과 같이 수행할 수 있습니다:

// 위에서 정의한 MyClaims 구조체를 사용합니다.
tokenString := "your-JWT-string"

// tokenString을 구문 분석하는 데 jwt 패키지가 사용할 함수를 정의해야 합니다
keyFunc := func(t *jwt.Token) (interface{}, error) {
	// 예상된 서명 알고리즘을 사용하는지 확인합니다
	if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
	    return nil, fmt.Errorf("예기치 않은 서명 알고리즘: %v", t.Header["alg"])
	}
	// 서명에 사용된 jwt 토큰의 비밀 키를 []byte 형식으로 반환합니다. 이는 이전에 서명에 사용한 키와 일관성을 유지해야 합니다
	return mySigningKey, nil
}

// 토큰 구문 분석
claims := &MyClaims{}
parsedToken, err := jwt.ParseWithClaims(tokenString, claims, keyFunc)
if err != nil {
	log.Fatalf("구문 분석 오류: %v", err)
}

if !parsedToken.Valid {
    log.Fatalf("유효하지 않은 토큰")
}

// 이 단계에서 parsedToken은 확인되었으며, 클레임을 읽을 수 있습니다
fmt.Printf("사용자: %s, 관리자: %v\n", claims.Username, claims.Admin)

위 코드에서는 토큰 문자열, MyClaims 인스턴스 및 키 함수 keyFuncjwt.ParseWithClaims 함수에 제공했습니다. 이 함수는 서명을 확인하고 토큰을 구문 분석하여 토큰이 유효한 경우 claims 변수를 채웁니다.