#3 Golang JWT 토큰으로 인증 구현하기

2019. 12. 16. 00:08 개발 이야기/Golang

 

요즘 백엔드, api 서버의 인증 방식의 대세는

 

단연코 JWT 토큰인 것 같습니다.

 

 

session 방식의 인증은 다수의 사용자의 모든 session 정보

 

메모리에 가지고 있어야 하기 때문에 리소스 소모가 많고,

 

인증 api를 범용으로 사용하기 위해서는 조금 어려운 면이 있기 때문인데요!

 

인증 api를 범용으로 사용한다는 말은

 

gmail 인증 후 받은 토큰을 다른 곳에 사용한다거나

 

여러 도메인의 인증을 한곳의 api에서 사용한다거나

 

이런 식으로 이용하는 것을 말합니다!

 

JWT 토큰이 뭔지 알아보겠습니다

 


JSON 웹 토큰이란 무엇입니까?

JWT (JSON Web Token)는 당사자간에 정보를 안전하게 JSON 객체로 전송하기위한 간결하고 독립적인 방법을 정의하는 개방형 표준 ( RFC 7519 )입니다. 이 정보는 디지털 서명되어 있으므로 확인하고 신뢰할 수 있습니다. JWT는 비밀 ( HMAC 알고리즘 사용) 또는 RSA 또는 ECDSA를 사용하는 공개 / 개인 키 쌍을 사용하여 서명 할 수 있습니다.

JWT를 암호화하여 당사자 간의 보안을 제공할 수도 있지만 서명된 토큰에 중점을 둘 것입니다. 서명된 토큰은 그 안에 포함된 클레임의 무결성을 확인할 수 있는 반면 암호화된 토큰은 해당 클레임을 다른 당사자로부터 숨 깁니다. 공개 / 개인 키 쌍을 사용하여 토큰에 서명하면 서명은 개인 키를 보유한 당사자만 서명 한 당사자임을 인증합니다.

 


 

JWT 토큰에는 더, Claim, 서명 세 부분으로 구성되어있는데요!

 

헤더에는 JWT 토큰의 유형과 해시, RSA와 같은 서명 알고리즘으로 구성되어 있고

 

Claim이라는 부분이 존재하는데요 이 부분에는

개발자의 마음대로 사용자의 정보를 저장할 수 있습니다

 

그리고 서명 부분에는 토큰을 검증할 수 있도록 메시지가 변경되지 않았는지

 

확인하는 역할을 하는 부분입니다.

 

자 그럼 이 토큰을 어떻게 사용하는지 Golang으로 작성하여 

 

만들어보도록 하겠습니다!!

 

"github.com/dgrijalva/jwt-go" 사용

 

먼저 회원가입 부분은 오늘의 JWT 인증 로그인 부분 설명과 맞지 않기 때문에

 

패쓰~~~ 하고 로그인 부분부터 설명하도록 하겠습니다.

 

 

먼저 저의 테스트 서버에는 test6@test.com 으로 가입되고

 

비밀번호가 123인 계정이 존재하는데요!

 

JSON 방식으로 보면 아래와 같습니다.

{
        "email": "test6@test.com",
        "password": "123"
}

 

함께 디버깅하면서 어떻게 돌아가는지 확인해보겠습니다!

 

 

저는 로그인 시 계정 확인 로직을 아래와 같이 작성했는데요!

func (user *User) LoginUser() bool {
    db, err := ConnectToDb()
    if err != nil {
        log.Println(err)
    }
    defer db.Close()

    _ = db.QueryRow(
        "SELECT user_no, user_name "+
            "FROM user_info "+
            "WHERE user_id = $1 AND user_pw = $2 AND is_enabled = 1",
        user.Email, sha512hash(user.Pw)).Scan(&user.UserNo, &user.Name)

    if user.UserNo != 0 {
        return true
    } else {
        return false
    }
}

비밀번호는 해쉬 처리(salt 값 섞음)를 해서 해쉬 한 값과

DB에 있는 해쉬 된 값을 비교해서 확인을 했습니다

 

그 후에 LoginUser 함수의 결과에 따라 JWT 토큰 만드는 데요!

if loginResult {
        jwtToken, err := user.GetJwtToken()
        if err != nil {
            c.JSON(http.StatusUnauthorized,
                gin.H{"status": http.StatusUnauthorized, "error": "Authentication failed"})
            return
        }

 

 

JWT 토큰을 만드는 로직은 아래와 같습니다

type Claims struct {
    UserNo int
    jwt.StandardClaims
}
var expirationTime = 5 * time.Minute

var JwtKey = []byte("JWT key 값을 넣어주세요~~")

func (user *User) GetJwtToken() (string, error) {
    // Declare the expiration time of the token
    // here, we have kept it as 5 minutes
    expirationTime := time.Now().Add(expirationTime)
    // Create the JWT claims, which includes the username and expiry time
    claims := &Claims{
        UserNo: user.UserNo,
        StandardClaims: jwt.StandardClaims{
            // In JWT, the expiry time is expressed as unix milliseconds
            ExpiresAt: expirationTime.Unix(),
        },
    }

    // Declare the token with the algorithm used for signing, and the claims
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    // Create the JWT string
    tokenString, err := token.SignedString(JwtKey)
    if err != nil {
        // If there is an error in creating the JWT return an internal server error
        // w.WriteHeader(http.StatusInternalServerError)
        return "", fmt.Errorf("token signed Error")
    } else {
        return tokenString, nil
    }
}

Claims라는 구조체 안에는 저의 user_info 디비에 시리얼 넘버인 UserNo를 넣었는데요!

 

로직을 보시면 아시겠지만 expirationTime 변수에 5분의 시간을 더해서 넣고

claims 구조체 안에 해당 결과를 넣어서 

jwt 토큰을 제너레이트 하고 이 토큰을 JwtKey를 이용해 Sign 하고

 

이 토큰 값을 리턴해서 set-cookie에 넣고 reponse를 해줍니다!

 

        c.Header("Cache-Control", "no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0")
        c.Header("Last-Modified", time.Now().String())
        c.Header("Pragma", "no-cache")
        c.Header("Expires", "-1")
        c.SetCookie("access-token", jwtToken, 1800, "", "", false, false)

        c.JSON(http.StatusOK, gin.H{
            "status": http.StatusOK,
            "isOK":   1,

 

이렇게 하면 userNo가 담긴 JWT 토큰을 만들 수 있는데요!

 

로그인 시도를 하고 토큰이 어떻게 날아오는지 확인해보겠습니다.

 

 

컴파일 후 바이너리 실행 모습

 

 

DB에 등록된 계정을 가지고 로그인 시도를 했습니다 

 

위와 같이 access-token이라는 이름의 토큰이 날아온 것을 확인할 수 있는데요

 

이 토큰을 어떻게 인증에 이용할까요?

 

인증 후 이용이 가능한 api에 

 

토큰을 검증하는 로직을 넣어주는데요

    token, err := c.Request.Cookie("access-token")
    if err != nil {
        // c.JSON(http.StatusUnauthorized,
        //     gin.H{"status": http.StatusUnauthorized, "error": "Authentication failed"})
        return 0, http.StatusUnauthorized
    }
    // log.Println(token.Value)
    // Get the JWT string from the cookie
    tknStr := token.Value

    if tknStr == "" {
        return 0, http.StatusUnauthorized
    }

    // Initialize a new instance of `Claims`
    claims := &Claims{}

    // Parse the JWT string and store the result in `claims`.
    // Note that we are passing the key in this method as well. This method will return an error
    // if the token is invalid (if it has expired according to the expiry time we set on sign in),
    // or if the signature does not match
    _, err = jwt.ParseWithClaims(tknStr, claims, func(token *jwt.Token) (interface{}, error) {
        return JwtKey, nil
    })

코드와 같이 access-token을 가져와서 

jwt 토큰 값을 claims 구조체 안에 파싱을 해줍니다.

    if err != nil {
        if err == jwt.ErrSignatureInvalid {
            return 0, http.StatusUnauthorized
        }

        log.Println(err)
        return 0, http.StatusForbidden
    } else {

        return claims.UserNo, http.StatusOK
    }

 

그러면 claims.UserNo에 디비에서 사용하는 userNo가 파싱 되겠죠!?

 

 

저는 이 정보를 이용해 사용자 정보를 가져오는 쿼리로 사용하고 있습니다

 

디버깅을 해서 watches 기능을 이용해 어떤 값이 들어가 있는지 보겠습니다

token에는 리퀘스트할 때 날아간 토큰 값이 존재하고요!

 

claims에는 UserNo가 7로 들어가 있는 것을 볼 수 있습니다!

 

이런 식으로 인증이 필요한 부분에는 토큰 검증 로직을 태워서

 

토큰이 유요 한 지 검증한 후 사용자 정보 조회 등에 이용할 수 있습니다!

 

이상으로 JWT 토큰을 이용해서 Golang으로 인증 로직을 구현해 보았습니다!