JWT란 무엇인가? 그리고 어떻게 사용하는가? (4) - 예제

Eon Kim·2021년 10월 13일
2

JWT

목록 보기
4/4
post-thumbnail

안녕하세요, 주니어 개발자 Eon입니다.

오늘 포스트 내용은 golang iris framework를 이용한 jwt 구현입니다.
아래 소스 코드는 docs에 나와 있는 예제 코드입니다.
golang iris framework jwt examples and usages
docs에 포함돼 있는 github 페이지를 참고하시면 쉽게 구현 가능합니다.

The reason why I chose iris : 왜 iris를 선택했는지 (사용하게 된 계기)

회사에서 golang iris framework 기반 mvc 패턴으로 만들어진 REST-API 서버를 개발했습니다.
해당 서버에 보안 로직이 필요했고, 간단하게 구현 가능한 jwt를 사용했습니다.
iris framework보다 다른 framework로 구현된 jwt가 가독성이 더 좋고, 사용 방법이 쉬워서 그걸 사용했습니다.
gin framework로 구현된 jwt였습니다.
golang gin framework jwt tutorial
다른 예제들과 다르게 rsa 관련 키를 넣을 필요도 없고, os.Setenv 세팅으로 키 값을 설정하는 방법이었습니다.
그래서 저는 gin framework로 구현된 것을 iris framework로 개발된 서버 코드에 이식했습니다.
왜 iris framework에서 지원하는 jwt를 사용하지 않았는가?

  • 별로 쓰고 싶지 않았습니다.
    모든 request API에 대하여 인증 로직을 커스터마이징하기 좋은 것이 gin framework로 구현된 소스 코드였습니다.

다행히도 두 framework 간에 호환성 문제는 겪지 않았습니다.
물론 여러 가지 변경한 것들이 있습니다.
이 부분은 나중에 추가적으로 다루겠습니다.
(거꾸로 되짚어 가려니 헷갈립니다....)

아래는 iris framework로 구현된 jwt 예제입니다.
Go 세팅만 잘 돼 있으시다면 구동하는 데에 문제는 없을 겁니다.
주석이 전부 달려 있기 때문에 따로 설명드릴 건 없을 것 같습니다.
토큰 길이가 꽤 긴데도 불구하고 uri로 request하는 것이기 때문에 좀 지저분합니다.

golang iris framework jwt example

package main

import (
	"fmt"
	"time"

	"github.com/kataras/iris/v12"
	"github.com/kataras/iris/v12/middleware/jwt"
)

const (
	accessTokenMaxAge  = 10 * time.Minute
	refreshTokenMaxAge = time.Hour
)

var (
	privateKey, publicKey = jwt.MustLoadRSA("rsa_private_key.pem", "rsa_public_key.pem")

	signer   = jwt.NewSigner(jwt.RS256, privateKey, accessTokenMaxAge)
	verifier = jwt.NewVerifier(jwt.RS256, publicKey)
)

// UserClaims a custom access claims structure.
type UserClaims struct {
	ID string `json:"user_id"`
	// Do: `json:"username,required"` to have this field required
	// or see the Validate method below instead.
	Username string `json:"username"`
}

// GetID implements the partial context user's ID interface.
// Note that if claims were a map then the claims value converted to UserClaims
// and no need to implement any method.
//
// This is useful when multiple auth methods are used (e.g. basic auth, jwt)
// but they all share a couple of methods.
func (u *UserClaims) GetID() string {
	return u.ID
}

// GetUsername implements the partial context user's Username interface.
func (u *UserClaims) GetUsername() string {
	return u.Username
}

// Validate completes the middleware's custom ClaimsValidator.
// It will not accept a token which its claims missing the username field
// (useful to not accept refresh tokens generated by the same algorithm).
func (u *UserClaims) Validate() error {
	if u.Username == "" {
		return fmt.Errorf("username field is missing")
	}

	return nil
}

// For refresh token, we will just use the jwt.Claims
// structure which contains the standard JWT fields.

func main() {
	app := iris.New()
	app.OnErrorCode(iris.StatusUnauthorized, handleUnauthorized)

	app.Get("/authenticate", generateTokenPair)
	app.Get("/refresh", refreshToken)

	protectedAPI := app.Party("/protected")
	{
		verifyMiddleware := verifier.Verify(func() interface{} {
			return new(UserClaims)
		})

		protectedAPI.Use(verifyMiddleware)

		protectedAPI.Get("/", func(ctx iris.Context) {
			// Access the claims through: jwt.Get:
			// claims := jwt.Get(ctx).(*UserClaims)
			// ctx.Writef("Username: %s\n", claims.Username)
			//
			// OR through context's user (if at least one method was implement by our UserClaims):
			user := ctx.User()
			id, _ := user.GetID()
			username, _ := user.GetUsername()
			ctx.Writef("ID: %s\nUsername: %s\n", id, username)
		})
	}

	// http://localhost:8080/protected (401)
	// http://localhost:8080/authenticate (200) (response JSON {access_token, refresh_token})
	// http://localhost:8080/protected?token={access_token} (200)
	// http://localhost:8080/protected?token={refresh_token} (401)
	// http://localhost:8080/refresh?refresh_token={refresh_token}
	// OR http://localhost:8080/refresh (request JSON{refresh_token = {refresh_token}}) (200) (response JSON {access_token, refresh_token})
	// http://localhost:8080/refresh?refresh_token={access_token} (401)
	app.Listen(":8080")
}

func generateTokenPair(ctx iris.Context) {
	// Simulate a user...
	userID := "53afcf05-38a3-43c3-82af-8bbbe0e4a149"

	// Map the current user with the refresh token,
	// so we make sure, on refresh route, that this refresh token owns
	// to that user before re-generate.
	refreshClaims := jwt.Claims{Subject: userID}

	accessClaims := UserClaims{
		ID:       userID,
		Username: "vamos-eon",
	}

	// Generates a Token Pair, long-live for refresh tokens, e.g. 1 hour.
	// First argument is the access claims,
	// second argument is the refresh claims,
	// third argument is the refresh max age.
	tokenPair, err := signer.NewTokenPair(accessClaims, refreshClaims, refreshTokenMaxAge)
	if err != nil {
		ctx.Application().Logger().Errorf("token pair: %v", err)
		ctx.StopWithStatus(iris.StatusInternalServerError)
		return
	}

	// Send the generated token pair to the client.
	// The tokenPair looks like: {"access_token": $token, "refresh_token": $token}
	ctx.JSON(tokenPair)
}

// There are various methods of refresh token, depending on the application requirements.
// In this example we will accept a refresh token only, we will verify only a refresh token
// and we re-generate a whole new pair. An alternative would be to accept a token pair
// of both access and refresh tokens, verify the refresh, verify the access with a Leeway time
// and check if its going to expire soon, then generate a single access token.
func refreshToken(ctx iris.Context) {
	// Assuming you have access to the current user, e.g. sessions.
	//
	// Simulate a database call against our jwt subject
	// to make sure that this refresh token is a pair generated by this user.
	// * Note: You can remove the ExpectSubject and do this validation later on by yourself.
	currentUserID := "53afcf05-38a3-43c3-82af-8bbbe0e4a149"

	// Get the refresh token from ?refresh_token=$token OR
	// the request body's JSON{"refresh_token": "$token"}.
	refreshToken := []byte(ctx.URLParam("refresh_token"))
	if len(refreshToken) == 0 {
		// You can read the whole body with ctx.GetBody/ReadBody too.
		var tokenPair jwt.TokenPair
		if err := ctx.ReadJSON(&tokenPair); err != nil {
			ctx.StopWithError(iris.StatusBadRequest, err)
			return
		}

		refreshToken = tokenPair.RefreshToken
	}

	// Verify the refresh token, which its subject MUST match the "currentUserID".
	_, err := verifier.VerifyToken(refreshToken, jwt.Expected{Subject: currentUserID})
	if err != nil {
		ctx.Application().Logger().Errorf("verify refresh token: %v", err)
		ctx.StatusCode(iris.StatusUnauthorized)
		return
	}

	/* Custom validation checks can be performed after Verify calls too:
	currentUserID := "53afcf05-38a3-43c3-82af-8bbbe0e4a149"
	userID := verifiedToken.StandardClaims.Subject
	if userID != currentUserID {
		ctx.StopWithStatus(iris.StatusUnauthorized)
		return
	}
	*/

	// All OK, re-generate the new pair and send to client,
	// we could only generate an access token as well.
	generateTokenPair(ctx)
}

func handleUnauthorized(ctx iris.Context) {
	if err := ctx.GetErr(); err != nil {
		ctx.Application().Logger().Errorf("unauthorized: %v", err)
	}

	ctx.WriteString("Unauthorized")
}

위의 jwt 예제를 직접 해보신다면 jwt를 어디에 써야 하는지, 어떻게 써야 하는지 조금 더 직접적으로 감이 잡히실 거라 생각합니다.
꼭 iris framework로 해야만 하는 것이 아니기 때문에 다른 framework로라도 해보시는 걸 추천합니다.
iris framework는 벤치마크 관련해서 여러 이슈가 있었던 framework인 만큼 그다지 추천드리고 싶지는 않습니다.
위에 링크로 걸어둔 gin framework로 구현하는 튜토리얼도 보시면 좋을 것 같습니다.
이것으로 jwt 포스팅을 마치겠습니다.
감사합니다.👍

profile
주니어 개발자

0개의 댓글