在 Gin 架構中使用 JWT 認證
什麼是JWT?
JWT全稱JSON Web Token是一種跨域認證解決方案,屬于一個開放的标準,它規定了一種Token實作方式,目前多用于前後端分離項目和OAuth2.0認證的業務場景下。
為什麼需要JWT?
在偏傳統的一些web項目中,我們通常使用的是
Cookie-Session
模式實作使用者認證。相關認證流程大緻如下:
1.使用者在浏覽器端填寫使用者名和密碼,并發送給服務端2.服務端對使用者名和密碼校驗通過後會生成一份儲存目前使用者相關資訊的session資料和一個與之對應的辨別(通常稱為session_id)3.服務端傳回響應時将上一步的session_id寫入使用者浏覽器的Cookie4.後續使用者來自該浏覽器的每次請求都會自動攜帶包含session_id的Cookie5.服務端通過請求中的session_id就能找到之前儲存的該使用者那份session資料,進而擷取該使用者的相關資訊。
這種方案依賴于用戶端(浏覽器)儲存Cookie,并且需要在服務端存儲使用者的Session資料。
在移動網際網路時代,我們的使用者可能使用浏覽器也可能使用APP來通路我們的服務,我們的web應用可能是前後端分開部署在不同的端口,有時候我們還需要支援第三方登入,這下
Cookie-Session
的模式就有些力不從心了。
JWT就是一種基于Token的輕量級認證模式,服務端認證通過後,會生成一個JSON對象,經過簽名後得到一個Token(令牌)再發回給使用者,使用者後續請求隻需要帶上這個Token,服務端解密之後就能擷取該使用者的相關資訊了。
生成JWT和解析JWT
我們在這裡直接使用
jwt-go
這個庫來實作我們生成JWT和解析JWT的功能。
定義需求
我們需要定制自己的需求來決定JWT中儲存哪些資料,比如我們規定在JWT中要存儲
username
資訊,那麼我們就定義一個
MyClaims
結構體如下:
// MyClaims 自定義聲明結構體并内嵌jwt.StandardClaims
// jwt包自帶的jwt.StandardClaims隻包含了官方字段
// 我們這裡需要額外記錄一個username字段,是以要自定義結構體
// 如果想要儲存更多資訊,都可以添加到這個結構體中
type MyClaims struct {
Username string `json:"username"`
jwt.StandardClaims
}
然後我們定義JWT的過期時間,這裡以2小時為例:
const TokenExpireDuration = time.Hour * 2
接下來還需要定義Secret:
var MySecret = []byte("夏天夏天悄悄過去")
生成JWT
// GenToken 生成JWT
func GenToken(username string) (string, error) {
// 建立一個我們自己的聲明
c := MyClaims{
"username", // 自定義字段
jwt.StandardClaims{
ExpiresAt: time.Now().Add(TokenExpireDuration).Unix(), // 過期時間
Issuer: "my-project", // 簽發人
},
}
// 使用指定的簽名方法建立簽名對象
token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
// 使用指定的secret簽名并獲得完整的編碼後的字元串token
return token.SignedString(MySecret)
}
解析JWT
// ParseToken 解析JWT
func ParseToken(tokenString string) (*MyClaims, error) {
// 解析token
token, err := jwt.ParseWithClaims(tokenString, &MyClaims{}, func(token *jwt.Token) (i interface{}, err error) {
return MySecret, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*MyClaims); ok && token.Valid { // 校驗token
return claims, nil
}
return nil, errors.New("invalid token")
}
在gin架構中使用JWT
首先我們注冊一條路由
/auth
,對外提供擷取Token的管道:
r.POST("/auth", authHandler)
我們的
authHandler
定義如下:
func authHandler(c *gin.Context) {
// 使用者發送使用者名和密碼過來
var user UserInfo
err := c.ShouldBind(&user)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"code": 2001,
"msg": "無效的參數",
})
return
}
// 校驗使用者名和密碼是否正确
if user.Username == "q1mi" && user.Password == "q1mi123" {
// 生成Token
tokenString, _ := GenToken(user.Username)
c.JSON(http.StatusOK, gin.H{
"code": 2000,
"msg": "success",
"data": gin.H{"token": tokenString},
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 2002,
"msg": "鑒權失敗",
})
return
}
使用者通過上面的接口擷取Token之後,後續就會攜帶着Token再來請求我們的其他接口,這個時候就需要對這些請求的Token進行校驗操作了,很顯然我們應該實作一個檢驗Token的中間件,具體實作如下:
// JWTAuthMiddleware 基于JWT的認證中間件
func JWTAuthMiddleware() func(c *gin.Context) {
return func(c *gin.Context) {
// 用戶端攜帶Token有三種方式 1.放在請求頭 2.放在請求體 3.放在URI
// 這裡假設Token放在Header的Authorization中,并使用Bearer開頭
// 這裡的具體實作方式要依據你的實際業務情況決定
authHeader := c.Request.Header.Get("Authorization")
if authHeader == "" {
c.JSON(http.StatusOK, gin.H{
"code": 2003,
"msg": "請求頭中auth為空",
})
c.Abort()
return
}
// 按空格分割
parts := strings.SplitN(authHeader, " ", 2)
if !(len(parts) == 2 && parts[0] == "Bearer") {
c.JSON(http.StatusOK, gin.H{
"code": 2004,
"msg": "請求頭中auth格式有誤",
})
c.Abort()
return
}
// parts[1]是擷取到的tokenString,我們使用之前定義好的解析JWT的函數來解析它
mc, err := ParseToken(parts[1])
if err != nil {
c.JSON(http.StatusOK, gin.H{
"code": 2005,
"msg": "無效的Token",
})
c.Abort()
return
}
// 将目前請求的username資訊儲存到請求的上下文c上
c.Set("username", mc.Username)
c.Next() // 後續的處理函數可以用過c.Get("username")來擷取目前請求的使用者資訊
}
}
注冊一個
/home
路由,發個請求驗證一下吧。
r.GET("/home", JWTAuthMiddleware(), homeHandler)
func homeHandler(c *gin.Context) {
username := c.MustGet("username").(string)
c.JSON(http.StatusOK, gin.H{
"code": 2000,
"msg": "success",
"data": gin.H{"username": username},
})
}
摘自:https://mp.weixin.qq.com/s/9hNAQsa8_nvVku72ofpZCg