iT邦幫忙

2022 iThome 鐵人賽

DAY 28
0
Software Development

30天學會Golang系列 第 28

Day28 - Go的 jwt 實作

  • 分享至 

  • xImage
  •  

jwt 實作

今天的內容是基於 day26day27 之上的,day26 講的是與資料庫的互動,day27 則是關於 jwt 的概念,所以如果覺得講的內容有所缺失,可以往前面幾篇看一下

整個程式碼的架構如下圖所示:

https://ithelp.ithome.com.tw/upload/images/20221009/20150797MH0mfKS1QI.png

今天主要是實作 jwt 的運作原理,在 jwt 的設定中,可以設定有效的持續時間,表示使用者成功登入後,產生一組 jwt token,回傳給前端,再由前端存取 jwt 至 storage,但因為目前我們是做伺服器端,所以做到回傳 jwt 給前端,因此我們產生 jwt 的位置會放在使用者成功登入後

// controllers/user/handler.go
// // 使用者登入
// // 如果資料庫中沒有找到對應的使用者帳密,回傳 err = record not found,有找到則 err = nil
// func LoginUser(email, password string) string {
// 	passwordSha1 := sha1It(password)
// 	user, err := User.LoginUser(email, passwordSha1)
// 	if err != nil {
// 		return "wrong email or password"
// 	}

	jwt, err := mJwt.GenerateJWT(user.Id, user.Email)
 	if err != nil {
 		fmt.Println("jwt error:", err)
 	}
 	return jwt
// }

// // 加密字串
var Secret = os.Getenv("SECRET")

// // sha加密
// func sha1It(password string) string {
// 	h := sha1.New()
// 	h.Write([]byte(password))
	// bs := h.Sum([]byte(Secret))
// 	encryptCode := fmt.Sprintf("%x", bs)
// 	return encryptCode
// }

實作關於 jwt 的部分就放在 middleware 的資料夾底下,首先產生 jwt 的部分主要是定義資料封包的樣子,在這邊,我們定義的資訊在 AuthClaims 與 GenerateJWT 中:

  • 發行者
  • 過期時間
  • 帳號
  • 使用者 id

此外實作解析 jwt 的方法 (JWTAuth) 以及取得 jwt 中的資訊 (GetUserId),這兩個的流程十分相似,基本上就是先確認這個 jwt 的正確性與是否在有效期限內,在 parseToken 函式中, expiresAt := time.Now().Add(10 * time.Second).Unix() 表示這個 token 的有效時間為 10 秒鐘,從產生 jwt 的那一刻開始計算,超過 10 秒後則過期

// middleware/jwt.go
package jwt

import (
	"fmt"
	"net/http"
	"os"
	"time"

	t "it/day28/app/utils/template"

	"github.com/gin-gonic/gin"
	"github.com/golang-jwt/jwt"
	_ "github.com/joho/godotenv/autoload"
)

var Secret = os.Getenv("SECRET")
var Issuer = os.Getenv("Issuer")

type AuthClaims struct {
	jwt.StandardClaims
	Email string
	Id    int
}

// 解析 jwt
func JWTAuth() gin.HandlerFunc {
	return func(c *gin.Context) {
		// 通過 http header 中的 token 解析認證
		token := c.Request.Header.Get("token")
		if token == "" {
			t.Template(c, http.StatusBadRequest, "not jwt token")
			c.Abort()
			return
		}

		// 解析 jwt 是否正確,如果不正確則提前結束,正確就繼續
		_, err := parseToken(token)
		if err != nil {
			var errMsg string
			if ve, ok := err.(*jwt.ValidationError); ok {
				if ve.Errors&jwt.ValidationErrorMalformed != 0 {
					errMsg = "token無效"
				} else if ve.Errors&jwt.ValidationErrorExpired != 0 {
					errMsg = "token過期"
				}
			}
			t.Template(c, http.StatusBadRequest, errMsg)
			c.Abort()
			return
		}
	}
}

// 獲取使用者資訊
func GetUserInfo(c *gin.Context) (*AuthClaims, error) {
	// 通過http header中的token解析來認證
	token := c.Request.Header.Get("token")
	if token == "" {
		return nil, fmt.Errorf("no jwt token")
	}

	claim, err := parseToken(token)
	if err != nil {
		return nil, fmt.Errorf("bad jwt: %s", err)
	}

	return claim, nil
}

// 解析 jwt token
func parseToken(token string) (*AuthClaims, error) {
	jwtToken, err := jwt.ParseWithClaims(token, &AuthClaims{}, func(token *jwt.Token) (i interface{}, e error) {
		return []byte(Secret), nil
	})
	if err == nil && jwtToken != nil {
		if claim, ok := jwtToken.Claims.(*AuthClaims); ok && jwtToken.Valid {
			return claim, nil
		}
	}
	return nil, err
}

// 產生 jwt
func GenerateJWT(id int, email string) (string, error) {
	expiresAt := time.Now().Add(10 * time.Second).Unix()
	claims := AuthClaims{
		Id:    id,
		Email: email,
		StandardClaims: jwt.StandardClaims{
			Issuer:    Issuer,
			ExpiresAt: expiresAt,
		},
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	tokenString, err := token.SignedString([]byte(Secret))
	if err != nil {
		return "", err
	}
	return tokenString, nil
}

環境變數定義如下:

// .env
DBUSER="root"
DBPASSWORD="1A2B3c4d"
DBHOST="127.0.0.1"
DBPORT="3306"
DBNAME="test_database"
SECRET="it"
Issuer="ithome"

另外新開一支驗證與取得使用者資訊的 api 來確認我們 jwt 的實作

// api/go
// 獲取 jwt token 中的資訊
func ApiGetUserInfo(c *gin.Context) {
	claim, err := mJwt.GetUserInfo(c)
	if err != nil {
		t.Template(c, http.StatusBadRequest, err)
	}
	t.Template(c, http.StatusOK, claim)
}

user.Use(mJwt.JWTAuth()) 表示在執行下面的 user.GET("/info", v1.ApiGetUserInfo) 前,會先執行 user.Use(mJwt.JWTAuth()),也是所謂的 middleware,middleware 可以當作因為這個函式很常會使用到,所以特別做成一包函式,透過 Use 的方式打包,這樣就可以避免下面假設有一堆的 api 都會用到,不斷的在各自 api 中重複引用這個函式

// router.go
//func InitRouter() *gin.Engine {
//	r := gin.Default()

//	// 定義前端打的 api 路徑
//	r.POST("/v1/register", v1.ApiRegister)
//	r.POST("/v1/login", v1.ApiLogin)

	// 需要用到 jwt 的打包在一起
	user := r.Group("/v1/user")
	user.Use(mJwt.JWTAuth())
	user.GET("/info", v1.ApiGetUserInfo)

//	return r
//}

那接下來我們就可以看看結果,首先是成功登入獲得 jwt 的畫面:

https://ithelp.ithome.com.tw/upload/images/20221009/20150797V34vVJSssm.png

我們可以先透過 解析 jwt 網站看一下我們獲得的 jwt 結果如何:

https://ithelp.ithome.com.tw/upload/images/20221009/20150797gJPfe06MhW.png

當取得 jwt ,我們將其複製放在 http 的 header 中,如果正確及在有效時間內的結果如下:

https://ithelp.ithome.com.tw/upload/images/20221009/20150797N9NRFqAOhD.png

如果過期的 jwt 則如下:

https://ithelp.ithome.com.tw/upload/images/20221009/20150797exDbfi6trm.png

第28天報到,之前都只有概念,還沒實作過 jwt,實作後發現這樣的設計是真的蠻不錯的

代碼連結

https://github.com/luckyuho/ithome30-golang/tree/main/day28


上一篇
Day27 - Go的 jwt 解說
下一篇
Day29 - Go的 channel day17 的還債 (上)
系列文
30天學會Golang31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言