GoLang에서 JWT토큰을 발급 받고, 검증 받는 방법 입니다.

라이브러리 받기

dgrijalva/jwt-go
Golang implementation of JSON Web Tokens (JWT). Contribute to dgrijalva/jwt-go development by creating an account on GitHub.
인자한 아저씨가 우릴 반겨줍니다.

사용할 라이브러리 입니다. 이를 다운 받아 줍니다.

go get github.com/dgrijalva/jwt-go/v4

모듈에 라이브러리가 추가되었습니다. 이 글에는 uuid를 사용하기위해 uuid 라이브러리도 사용했습니다.

[Lib] UUID
범용 고유 식별자 - 위키백과, 우리 모두의 백과사전Wikimedia Foundation, Inc.위키미디어 프로젝트 기여자[https://ko.wikipedia.org/wiki/%EB%B2%94%EC%9A%A9_%EA%B3%A0%EC%9C%A0_%EC%8B%9D%EB%B3%84%EC%9E%90]UUID는 분산 컴퓨팅 환경에서 중앙 통제 없이 개별적으로 고유의 id를 생성하기 위해 사용되는 식별자 입니다. 550d6400-e2zb-8g47-a716-446658772266 UUID 버전은 1, 3, 4, 5가 있습니다. 타임스…
uuid 참고
import (
	"github.com/dgrijalva/jwt-go/v4"
    "github.com/google/uuid"
)

라이브러리를 임포트 해줍니다.

토큰 발급

토큰 Claims(토큰 내부 정보) 를 정의할 구조체를 만듭니다.

type AuthTokenClaims struct {
	TokenUUID string   `json:"tid"`		// 토큰 UUID
	UserID    string   `json:"id"`		// 유저 ID
	Name      string   `json:"name"`	// 유저 이름
	Email     string   `json:"mail"`	// 유저 메일
	UserUUID  string   `json:"uid"`		// 유저 UUID
	Role      []string `json:"role"`	// 유저 역할
	jwt.StandardClaims			// 표준 토큰 Claims
}

jwt.StandardClaims 는 JWT 표준에 미리 정의된 Claims입니다. 그 외에 자신이 토큰에 담을 정보를 정의하면 됩니다. 저는 간단하게 한 유저의 정보를 담았습니다.

at := AuthTokenClaims{
	TokenUUID: uuid.NewString(),
	UserID:    "vompressor",
    
    // uuid 참고 // https://www.vompressor.com/uuid-go/
	UserUUID:  uuid.NewString(),
	Name:      "vompressor",
	Role:      []string {"user", "admin"},
	Email:     "[email protected]",
	StandardClaims: jwt.StandardClaims{
		ExpiresAt: jwt.At(time.Now().Add(time.Minute * 15)), // 만료시간 15분
	},
}

atoken := jwt.NewWithClaims(jwt.SigningMethodHS256, &at)
signedAuthToken, err := atoken.SignedString([]byte("SecretCode"))

jwt.NewWithClaims 함수에 원하는 서명 메소드와 자신이 정의한 구조체를 넣어줍니다. 이를 SignedString 함수에 전달하여 토큰을 서명합니다. 이 함수는 JWT 토큰을 String 으로 리턴합니다.

토큰 검증

토큰 검증은 jwt.ParseWithClaims 함수를 사용합니다. 기본적인 사용형태는 다음과 같습니다.

token, error := jwt.ParseWithClaims(tokenString, Claims, KeyFunction)
  • tokenString - 검증할 토큰 문자열
  • Claims - Claims 구조체 포인터
  • KeyFunction - 토큰 서명 검증전 검증 핸들러

KeyFunction 은 다음처럼 사용합니다.

func (token *jwt.Token) (interface{}, error) {
    if 잘못된 토큰일경우 {
        return nil, err    // secret 제공 거부, 에러 리턴
    }
    return []byte("SecretCode"), nil    // secret 제공
}

ex)

key := func (token *jwt.Token) (interface{}, error) {
    if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
        ErrUnexpectedSigningMethod = errors.New("unexpected signing method")
        return nil, ErrUnexpectedSigningMethod
    }
    return []byte("SecretCode"), nil
}

tok, err := jwt.ParseWithClaims(token, claims, key)
println(tok.Valid)

토큰의 헤더를 검증해서 서명 알고리즘이 다를경우, 서명을 거부하도록 구성했습니다.

검증된, 만료되지 않은 토큰일 경우 tok.Validtrue 아닐경우 false의 값을 가집니다.

사용 예시

Claims 정의

type AuthTokenClaims struct {
	TokenUUID string   `json:"tid"`
	UserID    string   `json:"id"`
	Name      string   `json:"name"`
	Email     string   `json:"mail"`
	UserUUID  string   `json:"uid"`
	Role      []string `json:"role"`

	jwt.StandardClaims
}

토큰 발급

func TestTokenBuild(t *testing.T) {
	at := AuthTokenClaims{
		TokenUUID: uuid.NewString(),
		UserID:    "vompressor",
		UserUUID:  uuid.NewString(),
		Name:      "vompressor",
		Role:      []string {"user", "admin"},
		Email:     "[email protected]",
		StandardClaims: jwt.StandardClaims{
			ExpiresAt: jwt.At(time.Now().Add(time.Minute * 15)),
		},
	}

	atoken := jwt.NewWithClaims(jwt.SigningMethodHS256, &at)
	signedAuthToken, err := atoken.SignedString([]byte("SecretCode"))

	if err != nil {
		t.Fatal(err)
	}

	t.Log(signedAuthToken)
}
=== RUN   TestTokenBuild
    /home/dev/project/xlog/xuth/lib/xjwt/tokk_test.go:42: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aWQiOiIzODdhYzBmYi00OTIzLTQ4MGYtYWEzZi1hZjY2NWVkMDdjMmYiLCJpZCI6InZvbXByZXNzb3IiLCJuYW1lIjoidm9tcHJlc3NvciIsIm1haWwiOiJ2b21wcmVzc29yQGdtYWlsLmNvbSIsInVpZCI6ImNmZTdjMjBiLWZkODUtNGQ2Zi05ZGNiLTFiZGZiOGM4ODI0MiIsInJvbGUiOlsidXNlciIsImFkbWluIl0sImV4cCI6MTYxNzk3Nzg2NS4xNzQ0NTA5fQ.fFOSMnPSYsxehmlR8blVA7LMr2zKQDCZblABqfCkg48
--- PASS: TestTokenBuild (0.00s)
PASS
jwt.io 에서 토큰을 복호화한 모습

토큰 검증

var token string = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aWQiOiIzODdhYzBmYi00OTIzLTQ4MGYtYWEzZi1hZjY2NWVkMDdjMmYiLCJpZCI6InZvbXByZXNzb3IiLCJuYW1lIjoidm9tcHJlc3NvciIsIm1haWwiOiJ2b21wcmVzc29yQGdtYWlsLmNvbSIsInVpZCI6ImNmZTdjMjBiLWZkODUtNGQ2Zi05ZGNiLTFiZGZiOGM4ODI0MiIsInJvbGUiOlsidXNlciIsImFkbWluIl0sImV4cCI6MTYxNzk3Nzg2NS4xNzQ0NTA5fQ.fFOSMnPSYsxehmlR8blVA7LMr2zKQDCZblABqfCkg48"

func TestTokenVerfy(t *testing.T) {
	claims := AuthTokenClaims{}
	key := func(token *jwt.Token) (interface{}, error) {
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, errors.New("Unexpected Signing Method")
		}
		return []byte("SecretCode"), nil
	}
	
	tok, err := jwt.ParseWithClaims(token, &claims, key)
	if err != nil {
		t.Fatal(err)
	}
	t.Logf("%t\n%#v", tok.Valid, claims)
}

정상적인 토큰

=== RUN   TestTokenVerfy
    /home/dev/project/xlog/xuth/lib/xjwt/tokk_test.go:60: true
        xjwt_test.AuthTokenClaims{TokenUUID:"387ac0fb-4923-480f-aa3f-af665ed07c2f", UserID:"vompressor", Name:"vompressor", Email:"[email protected]", UserUUID:"cfe7c20b-fd85-4d6f-9dcb-1bdfb8c88242", Role:[]string{"user", "admin"}, StandardClaims:jwt.StandardClaims{Audience:jwt.ClaimStrings(nil), ExpiresAt:(*jwt.Time)(0xc0000b6288), ID:"", IssuedAt:(*jwt.Time)(nil), Issuer:"", NotBefore:(*jwt.Time)(nil), Subject:""}}
--- PASS: TestTokenVerfy (0.00s)
PASS

타임아웃 토큰

=== RUN   TestTokenVerfy
    /home/dev/project/xlog/xuth/lib/xjwt/tokk_test.go:58: token is expired by 18.843229885s
--- FAIL: TestTokenVerfy (0.00s)
FAIL

HS256 메소드가 아닐경우

=== RUN   TestTokenVerfy
    /home/dev/project/xlog/xuth/lib/xjwt/tokk_test.go:58: token is unverifiable: Keyfunc returned an error
--- FAIL: TestTokenVerfy (0.00s)
FAIL

jwt.UnverfiableTokenError 로 리턴된 에러입니다.