Go實現(xiàn)JWT認(rèn)證中間件的項目實戰(zhàn)
在構(gòu)建安全可靠的 Go Web 服務(wù)時,JWT(JSON Web Token)認(rèn)證是常用的解決方案。本文將介紹如何在 Gin 框架中實現(xiàn)完整的 JWT 認(rèn)證方案,同時包含靈活的 Redis 集成選項。
一、為什么需要 JWT 中間件
JWT 作為現(xiàn)代 Web 開發(fā)的認(rèn)證標(biāo)準(zhǔn),相比傳統(tǒng) cookie + session 方式有幾個明顯優(yōu)勢:
- 無狀態(tài)性:服務(wù)器不需要存儲會話信息
- 跨域支持:天然支持跨域認(rèn)證
- 安全傳輸:基于簽名機制防止篡改
- 信息自包含:Token 本身攜帶用戶信息
在 Gin 框架中通過中間件實現(xiàn) JWT 認(rèn)證,可以統(tǒng)一處理認(rèn)證邏輯,避免每個路由重復(fù)編寫驗證代碼。
二、核心依賴包
開始前需要安裝如下包:
go get github.com/gin-gonic/gin go get github.com/golang-jwt/jwt/v5 go get github.com/redis/go-redis/v9 # 可選,按需安裝
三、實現(xiàn)方案設(shè)計
在實現(xiàn) JWT 認(rèn)證中間件時,我們的設(shè)計方案需要兼顧靈活性和安全性。整個流程可以分為幾個關(guān)鍵步驟:
- 初始化配置:從配置文件或環(huán)境變量中加載 JWT 的配置(如密鑰、簽發(fā)者、簽名算法、過期時間等)。我們使用單例模式確保配置只加載一次,并通過互斥鎖保證并發(fā)安全。
- 中間件流程:
- 排除特定路由:對于不需要認(rèn)證的路由(如登錄、公開資源),直接跳過 JWT 驗證。
- 解析 Authorization 頭:從請求頭中提取 Bearer Token,并驗證其格式是否正確。
- 驗證 Token:根據(jù)是否啟用 Redis,采用不同的驗證方式:
- 如果啟用了 Redis,首先嘗試從 Redis 中獲取該 Token 對應(yīng)的聲明(claims)。如果存在且有效,則直接使用;如果不存在或無效,則回退到JWT庫的驗證方式。
- 如果沒有啟用 Redis,則直接使用 JWT 庫驗證 Token 的簽名和有效期。
- 處理驗證結(jié)果:如果驗證通過,將 claims 存儲到 Gin 的上下文中,供后續(xù)處理函數(shù)使用;如果驗證失敗,則根據(jù)具體的錯誤類型返回相應(yīng)的錯誤信息。
- Token 生成:在用戶登錄成功后,生成 JWT Token。Token 中包含用戶的身份信息(如用戶ID和用戶名)以及 JWT 的標(biāo)準(zhǔn)聲明(如過期時間、簽發(fā)者等)。如果啟用了 Redis,還需要將 Token 和對應(yīng)的聲明存儲到 Redis 中,并設(shè)置與Token相同的過期時間。
- 錯誤處理:針對 JWT 驗證過程中可能出現(xiàn)的錯誤(如 Token 過期、格式錯誤、簽名無效等),提供清晰的錯誤信息,方便前端處理。
- 配置管理:提供重置配置的功能,以便在需要時(如密鑰輪換)重新加載配置。
為了更直觀地理解上述流程,下面用一個流程圖表示:

四、實戰(zhàn)
1. 配置結(jié)構(gòu)定義
// JWT核心配置
type JWTConfig struct {
Secret []byte // 加密密鑰 - 建議使用32字節(jié)安全隨機數(shù)
Issuer string // 簽發(fā)者 - 通常為服務(wù)名稱
SigningMethod jwt.SigningMethod // 簽名算法 - 支持HS256/HS384/HS512
ExpirationTime time.Duration // 有效時長 - 如24h, 15m等
}
// 自定義Claims結(jié)構(gòu)
type CustomClaims struct {
UserID int `json:"userID"` // 用戶ID
UserName string `json:"userName"` // 用戶名
jwt.RegisteredClaims // JWT標(biāo)準(zhǔn)字段
}
// 全局配置實例(線程安全)
var (
jwtConfig *JWTConfig
mutex sync.Mutex
)
2. JWT 中間件實現(xiàn)
func JwtMiddleware() gin.HandlerFunc {
// 定義排除路徑(支持通配符)
excludedPaths := map[string]bool{
"/api/v1/login": true,
"/public/*": true,
"/healthcheck": true,
}
return func(c *gin.Context) {
// 檢查當(dāng)前路徑是否在排除列表中
for path := range excludedPaths {
if match, _ := filepath.Match(path, c.Request.URL.Path); match {
c.Next() // 放行請求
return
}
}
// 獲取Authorization頭
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"code": 40101,
"message": "Authorization header is required",
})
return
}
// 解析Bearer Token
tokenString, err := parseBearerToken(authHeader)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"code": 40102,
"message": "Invalid token format",
})
return
}
// 驗證Token
claims, err := validateJWT(tokenString)
if err != nil {
handleJWTError(c, err) // 處理各類驗證錯誤
return
}
// 存儲claims到上下文(后續(xù)路由可通過c.Get("jwt_claims")獲取)
c.Set("jwt_claims", claims)
c.Next()
}
}
3. Token 生成
// 登錄成功時調(diào)用
func GenerateToken(userID int, userName string) (string, error) {
conf := config.LoadConfig() // 加載應(yīng)用配置
jwtConf, err := loadJwtConfig(conf)
if err != nil {
return "", fmt.Errorf("failed to load JWT config: %w", err)
}
// 創(chuàng)建Claims對象
claims := CustomClaims{
UserID: userID,
UserName: userName,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(jwtConf.ExpirationTime)),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: jwtConf.Issuer,
// 可添加更多聲明如:Subject, Audience等
},
}
// 創(chuàng)建并簽名Token
token := jwt.NewWithClaims(jwtConf.SigningMethod, claims)
tokenString, err := token.SignedString(jwtConf.Secret)
if err != nil {
return "", fmt.Errorf("failed to sign token: %w", err)
}
// 可選:當(dāng)Redis啟用時存儲Token
if conf.Redis.Enable {
redisCli := redis.GetRedisCli() // 獲取Redis連接
defer redisCli.Close()
// 序列化Claims
claimsJSON, err := json.Marshal(claims)
if err != nil {
log.Printf("Failed to marshal claims: %v", err)
// 不阻斷流程,僅記錄錯誤
} else {
// 存儲到Redis,使用Token作為Key
err = redisCli.Set(context.Background(), tokenString, claimsJSON, jwtConf.ExpirationTime).Err()
if err != nil {
log.Printf("Redis set error: %v", err)
}
}
}
return tokenString, nil
}
4. Token 驗證邏輯
func validateJWT(tokenString string) (*CustomClaims, error) {
conf := config.LoadConfig()
// 優(yōu)先從Redis獲?。ㄈ绻麊⒂茫?
if conf.Redis.Enable {
redisCli := redis.GetRedisCli()
defer redisCli.Close()
// 嘗試從Redis獲取
val, err := redisCli.Get(context.Background(), tokenString).Result()
if err == nil {
var claims CustomClaims
if err := json.Unmarshal([]byte(val), &claims); err == nil {
// 檢查過期時間
if claims.ExpiresAt != nil && claims.ExpiresAt.Before(time.Now()) {
return nil, jwt.ErrTokenExpired
}
return &claims, nil
}
}
// Redis查找失敗不影響后續(xù)流程
}
// JWT庫驗證
token, err := jwt.ParseWithClaims(
tokenString,
&CustomClaims{},
func(token *jwt.Token) (interface{}, error) {
// 驗證簽名算法是否匹配
if token.Method != jwtConfig.SigningMethod {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return jwtConfig.Secret, nil
},
)
if err != nil {
return nil, err
}
// 驗證Claims結(jié)構(gòu)
if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
return claims, nil
}
return nil, jwt.ErrTokenInvalidClaims
}
5. 錯誤處理機制
func handleJWTError(c *gin.Context, err error) {
var errorResponse gin.H
switch {
case errors.Is(err, jwt.ErrTokenExpired):
errorResponse = gin.H{
"code": 40103,
"message": "Token expired",
"action": "refresh_token",
}
case errors.Is(err, jwt.ErrTokenMalformed):
errorResponse = gin.H{
"code": 40104,
"message": "Malformed token",
}
case errors.Is(err, jwt.ErrTokenSignatureInvalid):
errorResponse = gin.H{
"code": 40105,
"message": "Invalid signature",
}
default:
errorResponse = gin.H{
"code": 40100,
"message": "Authentication failed",
}
}
c.AbortWithStatusJSON(http.StatusUnauthorized, errorResponse)
}
6. 輔助函數(shù)實現(xiàn)
// Bearer Token解析
func parseBearerToken(header string) (string, error) {
const bearerPrefix = "Bearer "
if len(header) <= len(bearerPrefix) || !strings.HasPrefix(header, bearerPrefix) {
return "", fmt.Errorf("authorization header format must be 'Bearer {token}'")
}
return strings.TrimSpace(header[len(bearerPrefix):]), nil
}
// 配置加載與初始化
func loadJwtConfig(conf *config.Config) (*JWTConfig, error) {
mutex.Lock()
defer mutex.Unlock()
// 如果已初始化,直接返回
if jwtConfig != nil {
return jwtConfig, nil
}
// 生成32字節(jié)安全密鑰
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
fmt.Println("failed to generate secure secret")
}
secret := base64.URLEncoding.EncodeToString(b)
// 解析簽名算法
signingMethod := jwt.GetSigningMethod(conf.Jwt.SigningMethod)
if signingMethod == nil {
return nil, fmt.Errorf("invalid signing method")
}
// 解析過期時間
expirationTime, err := time.ParseDuration(conf.Jwt.ExpirationTime)
if err != nil {
return nil, fmt.Errorf("invalid expiration format: %w", err)
}
// 創(chuàng)建配置實例
jwtConfig = &JWTConfig{
Secret: secret,
Issuer: conf.Jwt.Issuer,
SigningMethod: signingMethod,
ExpirationTime: expirationTime,
}
return jwtConfig, nil
}
// 重置配置(用于密鑰輪換)
func ResetJWTConfig() {
mutex.Lock()
defer mutex.Unlock()
jwtConfig = nil
}
7. config 配置文件
# config.yaml 示例 # redis配置 redis: enable: false # 是否啟用 redis addr: localhost:6379 password: db: 0 # jwt配置 jwt: issuer: vespeng # 簽發(fā)者 signingMethod: HS256 # 簽名算法 (HS256、HS384、HS512) expirationTime: 30m # 過期時間 (單位 min)
具體加載配置文件可參考 Go 搭建高效的 Gin Web 目錄結(jié)構(gòu)
五、在 Gin 路由中使用
func main() {
r := gin.Default()
// 應(yīng)用全局JWT中間件
r.Use(JwtMiddleware())
// 登錄路由(排除中間件)
r.POST("/login", func(c *gin.Context) {
// 1. 驗證用戶憑證(省略具體實現(xiàn))
user := authenticate(c.PostForm("username"), c.PostForm("password"))
if user == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
return
}
// 2. 生成JWT
token, err := GenerateToken(user.ID, user.Name)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Token generation failed"})
return
}
// 3. 返回響應(yīng)
c.JSON(http.StatusOK, gin.H{
"token": token,
"expires_in": int(jwtConfig.ExpirationTime.Seconds()),
})
})
// 需要認(rèn)證的路由
authGroup := r.Group("/api")
{
authGroup.GET("/business", func(c *gin.Context) {
// 從上下文獲取claims
rawClaims, exists := c.Get("jwt_claims")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Claims not found"})
return
}
// 類型斷言
claims, ok := rawClaims.(*CustomClaims)
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid claims format"})
return
}
// 返回用戶信息
c.JSON(http.StatusOK, gin.H{
"user_id": claims.UserID,
"username": claims.UserName,
})
})
// 其他需要認(rèn)證的路由...
}
// 啟動服務(wù)
r.Run(":8080")
}
以上實現(xiàn)方案既保證了安全性,又能保持代碼的整潔和可維護性。根據(jù)實際業(yè)務(wù)需求,你可以靈活調(diào)整過期時間、簽名算法等參數(shù),以滿足不同場景需求。
到此這篇關(guān)于Go實現(xiàn)JWT認(rèn)證中間件的項目實戰(zhàn)的文章就介紹到這了,更多相關(guān)Go JWT認(rèn)證中間件內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go?處理大數(shù)組使用?for?range?和?for?循環(huán)的區(qū)別
這篇文章主要介紹了Go處理大數(shù)組使用for?range和for循環(huán)的區(qū)別,對于遍歷大數(shù)組而言,for循環(huán)能比for?range循環(huán)更高效與穩(wěn)定,這一點在數(shù)組元素為結(jié)構(gòu)體類型更加明顯,下文具體分析感興趣得小伙伴可以參考一下2022-05-05
在go文件服務(wù)器加入http.StripPrefix的用途介紹
這篇文章主要介紹了在go文件服務(wù)器加入http.StripPrefix的用途介紹,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12
Golang開發(fā)Go依賴管理工具dep安裝驗證實現(xiàn)過程
這篇文章主要為大家介紹了Golang開發(fā)Go依賴管理工具dep安裝驗證及初始化一系列實現(xiàn)過程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步2021-11-11
golang有用的庫及工具 之 zap.Logger包的使用指南
這篇文章主要介紹了golang有用的庫及工具 之 zap.Logger包的使用指南,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12

