Go語言中Gin框架使用JWT實現(xiàn)登錄認(rèn)證的方案
Gin框架JWT登錄認(rèn)證
背景: 在如今前后端分離開發(fā)的大環(huán)境中,我們需要解決一些登陸,后期身份認(rèn)證以及鑒權(quán)相關(guān)的事情,通常的方案就是采用請求頭攜帶token的方式進行實現(xiàn)。
在開始學(xué)習(xí)JWT之前,我們可以先了解下早期的幾種方案。
1. token、cookie、session的區(qū)別
Cookie
Cookie總是保存在客戶端中,按在客戶端中的存儲位置,可分為內(nèi)存Cookie和硬盤Cookie。
內(nèi)存Cookie由瀏覽器維護,保存在內(nèi)存中,瀏覽器關(guān)閉后就消失了,其存在時間是短暫的。硬盤Cookie保存在硬盤里,有一個過期時間,除非用戶手工清理或到了過期時間,硬盤Cookie不會被刪除,其存在時間是長期的。所以,按存在時間,可分為非持久Cookie和持久Cookie。
cookie 是一個非常具體的東西,指的就是瀏覽器里面能永久存儲的一種數(shù)據(jù),僅僅是瀏覽器實現(xiàn)的一種數(shù)據(jù)存儲功能。
cookie由服務(wù)器生成,發(fā)送給瀏覽器,瀏覽器把cookie以key-value形式保存到某個目錄下的文本文件內(nèi),下一次請求同一網(wǎng)站時會把該cookie發(fā)送給服務(wù)器。由于cookie是存在客戶端上的,所以瀏覽器加入了一些限制確保cookie不會被惡意使用,同時不會占據(jù)太多磁盤空間,所以每個域的cookie數(shù)量是有限的。
Session
Session字面意思是會話,主要用來標(biāo)識自己的身份。比如在無狀態(tài)的api服務(wù)在多次請求數(shù)據(jù)庫時,如何知道是同一個用戶,這個就可以通過session的機制,服務(wù)器要知道當(dāng)前發(fā)請求給自己的是誰
為了區(qū)分客戶端請求,服務(wù)端會給具體的客戶端生成身份標(biāo)識session,然后客戶端每次向服務(wù)器發(fā)請求的時候,都帶上這個“身份標(biāo)識”,服務(wù)器就知道這個請求來自于誰了。至于客戶端如何保存該標(biāo)識,可以有很多方式,對于瀏覽器而言,一般都是使用cookie的方式
服務(wù)器使用session把用戶信息臨時保存了服務(wù)器上,用戶離開網(wǎng)站就會銷毀,這種憑證存儲方式相對于cookie來說更加安全,但是session會有一個缺陷: 如果web服務(wù)器做了負(fù)載均衡,那么下一個操作請求到了另一臺服務(wù)器的時候session會丟失。因此,通常企業(yè)里會使用redis,memcached緩存中間件來實現(xiàn)session的共享,此時web服務(wù)器就是一個完全無狀態(tài)的存在,所有的用戶憑證可以通過共享session的方式存取,當(dāng)前session的過期和銷毀機制需要用戶做控制。
Token
token的意思是“令牌”,是用戶身份的驗證方式,最簡單的token組成: uid(用戶唯一標(biāo)識)+time(當(dāng)前時間戳)+sign(簽名,由token的前幾位+鹽以哈希算法壓縮成一定長度的十六進制字符串),同時還可以將不變的參數(shù)也放進token
今天我們主要想講的就是Json Web Token,也就是本篇的主題:JWT
2. 什么是JWT
JWT: JSON Web Token,是一種用于身份驗證和授權(quán)的開放標(biāo)準(zhǔn),JWT可以在網(wǎng)絡(luò)應(yīng)用間安全的傳輸。JWT由三個部分組成:頭部(Header)、載荷(Payload)和簽名(Signature)
JWT具有可擴展性、簡單、輕量級、跨語言等優(yōu)點,是前后端分離框架中最常用的驗證方式。JWT工作流程大致如下:
1.當(dāng)用戶成功登錄后,服務(wù)器會生成一個JWT并返回給客戶端
2.客戶端將JWT儲存在本地
3.之后每次向服務(wù)器請求時都會在請求頭中攜帶JWT
4.服務(wù)器會驗證JWT的合法性,并根據(jù)其中的信息判斷用戶的身份和權(quán)限,從而決定是否允許用戶訪問請求的資源
JWT Token組成部分
header: 用來指定使用的算法alg(HMAC HS256 RS256)和token類型typ(如JWT)payload: 包含聲明(要求),聲明通常是用戶信息或其他數(shù)據(jù)的聲明,比如用戶id,名稱,郵箱等. 聲明可分為三種: registered,public,privatesignature: 用來保證JWT的真實性,可以使用不同的算法
header
{
“alg”: “HS256”,
“typ”: “JWT”
}
對上面的json進行base64編碼即可得到JWT的第一個部分
payload
載荷(Payload)用來表示需要傳遞的數(shù)據(jù),例如用戶ID、權(quán)限信息等,
包含聲明(claims),即用戶的相關(guān)信息。這些信息可以是公開的,也可以是私有的,但應(yīng)避免放入敏感信息,因為該部分可以被解碼查看。載荷中的聲明可以驗證,但不加密。
常用的字段如下:
Issuer:發(fā)行人,縮寫iss
ExpiresAt:過期時間,exp
Subject:主題信息,sub
NotBefore:在此時間之前不可以用,nbf
IssuedAt:發(fā)布時間,iat
ID:JWT的ID,jti
registered claims: 預(yù)定義的聲明,通常會放置一些預(yù)定義字段,比如過期時間,主題等(iss:issuer,exp:expiration time,sub:subject,aud:audience)public claims: 可以設(shè)置公開定義的字段private claims: 用于統(tǒng)一使用他們的各方之間的共享信息
{
“sub”: “xxx-api”,
“name”: “bgbiao.top”,
“admin”: true
}
對payload部分的json進行base64編碼后即可得到JWT的第二個部分
注意: 不要在header和payload中放置敏感信息,除非信息本身已經(jīng)做過脫敏處理
signature
為了得到簽名部分,必須有編碼過的header和payload,以及一個秘鑰,簽名算法使用header中指定的那個,然后對其進行簽名即可
Signature = HMAC SHA256(base64UrlEncode(header)+“.”+base64UrlEncode(payload),secret)
簽名是用于驗證消息在傳遞過程中有沒有被更改,并且,對于使用私鑰簽名的token,它還可以驗證JWT的發(fā)送方是否為它所稱的發(fā)送方。
JWT Token: base64(header).base64(payload).Signature
jwt官網(wǎng):https://jwt.io
下圖就是一個典型的jwt-token的組成部分。

3. 什么時候用JWT
- Authorization(授權(quán)): 典型場景,用戶請求的token中包含了該令牌允許的路由,服務(wù)和資源。單點登錄其實就是現(xiàn)在廣泛使用JWT的一個特性
- Information Exchange(信息交換): 對于安全的在各方之間傳輸信息而言,JSON Web Tokens無疑是一種很好的方式.因為JWTs可以被簽名,例如,用公鑰/私鑰對,你可以確定發(fā)送人就是它們所說的那個人。另外,由于簽名是使用頭和有效負(fù)載計算的,您還可以驗證內(nèi)容沒有被篡改。
JWT的工作流程

基于Token的身份認(rèn)證是無狀態(tài)的,服務(wù)器或者session中不會存儲任何用戶信息.(很好的解決了共享session的問題)
- 用戶攜帶用戶名和密碼請求獲取token(接口數(shù)據(jù)中可使用appId,appKey等)
- 服務(wù)端校驗用戶憑證,并返回用戶或客戶端一個Token
- 客戶端存儲token,并在請求頭中攜帶Token
- 服務(wù)端校驗token并返回數(shù)據(jù)
- 隨后客戶端的每次請求都需要使用token
- token應(yīng)該放在header中
所以,基本上整個過程分為兩個階段,第一個階段,客戶端向服務(wù)端獲取token,第二階段,客戶端帶著該token去請求相關(guān)的資源.
通常比較重要的是,服務(wù)端如何根據(jù)指定的規(guī)則進行token的生成。
在認(rèn)證的時候,當(dāng)用戶用他們的憑證成功登錄以后,一個JSON Web Token將會被返回。
此后,token就是用戶憑證了,你必須非常小心以防止出現(xiàn)安全問題。
一般而言,你保存令牌的時候不應(yīng)該超過你所需要它的時間。
無論何時用戶想要訪問受保護的路由或者資源的時候,用戶代理(通常是瀏覽器)都應(yīng)該帶上JWT,典型的,通常放在Authorization header中,用Bearer schema: Authorization: Bearer <token>
服務(wù)器上的受保護的路由將會檢查Authorization header中的JWT是否有效,如果有效,則用戶可以訪問受保護的資源。如果JWT包含足夠多的必需的數(shù)據(jù),那么就可以減少對某些操作的數(shù)據(jù)庫查詢的需要,盡管可能并不總是如此。
如果token是在授權(quán)頭(Authorization header)中發(fā)送的,那么跨源資源共享(CORS)將不會成為問題,因為它不使用cookie.

- 客戶端向授權(quán)接口請求授權(quán)
- 服務(wù)端授權(quán)后返回一個access token給客戶端
- 客戶端使用access token訪問受保護的資源
4. gin框架封裝jwt
我們在go官方提供的包里面搜jwt https://pkg.go.dev/
我們使用第一個最常用的

下載
go get -u github.com/golang-jwt/jwt/v5

jwt的功能很多,我們不用每個都搞清楚,目前只需要把examples里面的就可以了
我們先生成一個token,然后再去解析這個token

我們使用可以自定義參數(shù)的

1. 生成token
package jwtutil
import (
"github.com/golang-jwt/jwt/v5"
"jingtian/myproject/config"
"time"
)
// 這種不能用段變量方式創(chuàng)建
var mySigningKey = []byte(config.JwtSecretKey)
// MyCustomClaims 1.自定義聲明類型
type MyCustomClaims struct {
Username string `json:"username"`
jwt.RegisteredClaims
}
// GenToken 2. 封裝生成token的函數(shù)
// 根據(jù)官方定義,返回一個token和error
func GenToken(username string) (string, error) {
// Create claims with multiple fields populated
claims := MyCustomClaims{
username, //根據(jù)用戶名來動態(tài)生成
jwt.RegisteredClaims{
// A usual scenario is to set the expiration time relative to the current time
ExpiresAt: jwt.NewNumericDate(time.Now().Add(config.TokenExpire)), //過期時間,是個可變參數(shù)
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "jingtian",
Subject: "myjwt",
},
}
//生成token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
ss, err := token.SignedString(mySigningKey)
return ss, err
}

然后,在登錄的地方調(diào)用,先登錄,用戶名和密碼是對的情況下。生成token

2. 解析token
看下官網(wǎng)用法

我們使用第一個Custom

我們在config.go里面封裝成函數(shù)
// ParseToken 3.解析token
func ParseToken(tokenString string) (*MyCustomClaims, error) {
token, err := jwt.ParseWithClaims(tokenString, &MyCustomClaims{}, func(token *jwt.Token) (interface{}, error) {
// byte里面改成我們設(shè)置的key
return []byte(config.JwtSecretKey), nil
})
if err != nil {
//fmt.Println("解析token失敗", err.Error())
fields := map[string]interface{}{
"錯誤原因": err.Error(),
}
logs.Error(fields, "解析token失敗")
return nil, err
} else if claims, ok := token.Claims.(*MyCustomClaims); ok && token.Valid {
//說明token合法
//fmt.Println(claims.Username, claims.RegisteredClaims.Issuer)
return claims, nil
} else {
logs.Error(nil, "token不合法")
return nil, err
}
}

在main里面調(diào)用,測試
//驗證token是否合法
claims, tokenerr := jwtutil.ParseToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Imppbmd0aWFuIiwiaXNzIjoiamluZ3RpYW4iLCJzdWIiOiJteWp3dCIsImV4cCI6MTczMDM0Mjk3MiwibmJmIjoxNzMwMzQyODUyLCJpYXQiOjE3MzAzNDI4NTJ9.aqBEft2N1zkOISfQ-b1VvBDRnyhMiPZ17Ct-r0sNvgU")
if tokenerr != nil {
fmt.Println("token不合法: ", tokenerr)
} else {
fmt.Println("token合法:", claims)
}
使用合法的token驗證

我們設(shè)置的token過期時間是2分鐘,過兩分鐘再驗證
可以看到token不合法,已過期

登錄登出代碼
我們在router層寫路由信息
package auth
import (
"github.com/gin-gonic/gin"
"jingtian/myproject/controllers/auth"
)
// 實現(xiàn)登錄接口
func login(authGroup *gin.RouterGroup) {
//具體邏輯寫到控制器controller里面
authGroup.POST("/login", auth.Login)
}
// 實現(xiàn)登出接口
func loginout(authGroup *gin.RouterGroup) {
authGroup.GET("/loginout", auth.Loginout)
}
// RegisterSubRouter 認(rèn)證子路由
func RegisterSubRouter(g *gin.RouterGroup) {
//配置登錄功能路由策略
authGroup := g.Group("/auth")
login(authGroup)
loginout(authGroup)
}

在controllers.go里面寫具體的登錄登出邏輯
package auth
import (
"github.com/gin-gonic/gin"
"jingtian/myproject/utils/logs"
"net/http"
)
// UserInfo 創(chuàng)建結(jié)構(gòu)體,綁定用戶信息
type UserInfo struct {
Username string `json:"username"`
Password string `json:"password"`
}
// Login 登錄邏輯
func Login(c *gin.Context) {
//1.獲取前端傳來的用戶信息
var user UserInfo
//綁定結(jié)構(gòu)體 ShouldBing綁定,可以根據(jù)結(jié)構(gòu)體中的標(biāo)簽來 確定請求的content-type類型
if err := c.ShouldBind(&user); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
logs.Debug(map[string]interface{}{
"用戶名": user.Username,
"密碼": user.Password,
}, "開始驗證用戶登錄信息")
}
// Loginout 登出
func Loginout(c *gin.Context) {
//如果我們將token存到了redis里面,需要做清除邏輯,保存到內(nèi)存,只需要前端把存到本地的token刪掉即可
c.JSON(http.StatusOK, gin.H{
"code": 200,
"msg": "success",
})
logs.Debug(nil, "退出成功")
}

在routers.go里面調(diào)用

在main.go里面調(diào)用

運行,postman測試登錄接口

拿到數(shù)據(jù)

測試登出接口

登錄驗證
package auth
import (
"github.com/gin-gonic/gin"
"jingtian/myproject/utils/jwtutil"
"jingtian/myproject/utils/logs"
"net/http"
)
// UserInfo 創(chuàng)建結(jié)構(gòu)體,綁定用戶信息
type UserInfo struct {
Username string `json:"username"`
Password string `json:"password"`
}
// Login 登錄邏輯
func Login(c *gin.Context) {
//1.獲取前端傳來的用戶信息
var user UserInfo
//綁定結(jié)構(gòu)體 ShouldBing綁定,可以根據(jù)結(jié)構(gòu)體中的標(biāo)簽來 確定請求的content-type類型
if err := c.ShouldBind(&user); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
logs.Debug(map[string]interface{}{
"用戶名": user.Username,
"密碼": user.Password,
}, "開始驗證用戶登錄信息")
//登錄成功之后開始驗證,驗證通過生成token
//模擬從數(shù)據(jù)庫中查詢用戶名和密碼
if user.Username == "jingtian" && user.Password == "123456" {
logs.Info(nil, "用戶名密碼正確")
//生成token
ss, err := jwtutil.GenToken(user.Username)
if err != nil {
logs.Error(map[string]interface{}{
"用戶名": user.Username,
"錯誤信息": err.Error(),
}, "用戶名密碼正確,生成token失敗")
c.JSON(http.StatusOK, gin.H{
"error": err.Error(),
"status": 400,
})
return
}
logs.Info(nil, "用戶名密碼正確,生成token成功")
//將token返回給前端
data := make(map[string]interface{})
data["token"] = ss
c.JSON(http.StatusOK, gin.H{
"status": 200,
"data": data,
"msg": "登錄成功",
})
return
} else {
c.JSON(http.StatusOK, gin.H{
"status": 400,
"msg": "用戶名或密碼不正確",
})
return
}
}
// Loginout 登出
func Loginout(c *gin.Context) {
//如果我們將token存到了redis里面,需要做清除邏輯,保存到內(nèi)存,只需要前端把存到本地的token刪掉即可
c.JSON(http.StatusOK, gin.H{
"code": 200,
"msg": "success",
})
logs.Debug(nil, "退出成功")
}
用戶名和密碼都正確,返回token

當(dāng)用戶名或密碼不正確,拿不到token

登錄驗證成功后,前端在訪問其他接口的時候,都需要驗證是否攜帶正確的token
此時,我們需要通過中間件來驗證,除了登錄和登出的接口,其他接口都需要驗證
// Package middlewares 中間件層 配置中間件
package middlewares
import (
"fmt"
"github.com/gin-gonic/gin"
"jingtian/myproject/utils/jwtutil"
"jingtian/myproject/utils/logs"
"net/http"
)
// CheckToken 校驗jwt token
func CheckToken(c *gin.Context) {
//驗證token是否合法,除了login和loginout之外的請求,都要驗證token是否合法
//獲取請求路徑,c.FullPath()獲取請求群路徑 這個也可以c.Request.URL.Path
//requestUrl := c.FullPath()
requestUrl := c.Request.URL.Path
//requestUrl := c.FullPath()
logs.Debug(map[string]interface{}{
"url": requestUrl,
}, "獲取的請求路徑")
//我們可以做下判斷,當(dāng)請求路徑不是登錄或者登出的路徑時,就做token校驗
if requestUrl == "/api/auth/login" || requestUrl == "/api/auth/loginout" {
c.Next()
} else {
//其他接口需要驗證合法性
//token一般會存放在請求頭Header中的 Authorization字段中
//先獲取請求頭中是否包含該字段
//tokenString := c.Request.Header.Get("Authorization")
tokenString := c.GetHeader("Authorization")
if tokenString == "" {
c.JSON(http.StatusOK, gin.H{
"code": http.StatusUnauthorized,
"msg": "請求沒有攜帶token,請登錄后在嘗試",
})
c.Abort()
} else {
claims, tokenerr := jwtutil.ParseToken(tokenString)
if tokenerr != nil {
fmt.Println("token不合法: ", tokenerr)
c.JSON(http.StatusOK, gin.H{
"code": http.StatusUnauthorized,
"msg": "token不合法",
})
c.Abort()
} else {
//驗證通過的話,把claims放在Context里面
c.Set("claims", claims)
//其他的邏輯里面,如果需要獲取claims值,可以使用c.Get("claims")
c.Next()
fmt.Println("token合法:", claims)
}
}
}
}

正常的登錄登出,都不驗證token

登錄生成token

其他請求,不帶Authorization 請求頭的,一律攔截

帶上Authorization請求頭,但是token不合法的,也攔截

只有帶上Authorization請求頭,token也合法的請求,才能通過

以上就是Go語言中Gin框架使用JWT實現(xiàn)登錄認(rèn)證的方案的詳細(xì)內(nèi)容,更多關(guān)于Go Gin實現(xiàn)JWT登錄認(rèn)證的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
golang?gin框架實現(xiàn)大文件的流式上傳功能
這篇文章主要介紹了golang?gin框架中實現(xiàn)大文件的流式上傳,本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-07-07
Go語言中html/template模塊詳細(xì)功能介紹與示例代碼
這篇文章主要介紹了Go語言中html/template模塊詳細(xì)功能介紹與示例代碼,這里說的是go 語言中自帶的包html/template里的一些基本操作,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2025-03-03
golang中strconv.ParseInt函數(shù)用法示例
這篇文章主要介紹了golang中strconv.ParseInt函數(shù)用法,實例分析了strconv.ParseInt函數(shù)將字符串轉(zhuǎn)換為數(shù)字的簡單使用方法,需要的朋友可以參考下2016-07-07

