今天的內容是基於 day26 與 day27 之上的,day26 講的是與資料庫的互動,day27 則是關於 jwt 的概念,所以如果覺得講的內容有所缺失,可以往前面幾篇看一下
整個程式碼的架構如下圖所示:
今天主要是實作 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 中:
此外實作解析 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 的畫面:
我們可以先透過 解析 jwt 網站看一下我們獲得的 jwt 結果如何:
當取得 jwt ,我們將其複製放在 http 的 header 中,如果正確及在有效時間內的結果如下:
如果過期的 jwt 則如下:
https://github.com/luckyuho/ithome30-golang/tree/main/day28