Go語言實現雙Token登錄的思路與實現
引言
在現代Web應用中,身份認證是保障系統安全的重要環節。傳統的單Token認證方式存在一些安全隱患,如Token泄露可能導致長期風險。雙Token機制(Access Token + Refresh Token)提供了更好的安全性和用戶體驗。本文將介紹如何使用Go語言實現雙Token登錄系統。
雙Token機制概述
雙Token機制包含兩種令牌:
- Access Token:短期有效的令牌,用于訪問受保護資源
- Refresh Token:長期有效的令牌,用于獲取新的Access Token
這種機制的優勢在于:
- Access Token有效期短,即使泄露影響有限
- Refresh Token不直接用于資源訪問,降低了泄露風險
- 無需頻繁重新登錄,保持用戶體驗
實現思路
1. 數據結構設計
首先定義Token相關的數據結構:
type TokenDetails struct {AccessToken stringRefreshToken stringAccessUuid stringRefreshUuid stringAtExpires int64RtExpires int64
}type AccessDetails struct {AccessUuid stringUserId uint64
}
2. Token生成與存儲
使用JWT(JSON Web Token)生成Token,并存儲在Redis中:
func CreateToken(userid uint64) (*TokenDetails, error) {td := &TokenDetails{}td.AtExpires = time.Now().Add(time.Minute * 15).Unix()td.AccessUuid = uuid.New().String()td.RtExpires = time.Now().Add(time.Hour * 24 * 7).Unix()td.RefreshUuid = uuid.New().String()// 創建Access TokenatClaims := jwt.MapClaims{}atClaims["authorized"] = trueatClaims["access_uuid"] = td.AccessUuidatClaims["user_id"] = useridatClaims["exp"] = td.AtExpiresat := jwt.NewWithClaims(jwt.SigningMethodHS256, atClaims)td.AccessToken, _ = at.SignedString([]byte(os.Getenv("ACCESS_SECRET")))// 創建Refresh TokenrtClaims := jwt.MapClaims{}rtClaims["refresh_uuid"] = td.RefreshUuidrtClaims["user_id"] = useridrtClaims["exp"] = td.RtExpiresrt := jwt.NewWithClaims(jwt.SigningMethodHS256, rtClaims)td.RefreshToken, _ = rt.SignedString([]byte(os.Getenv("REFRESH_SECRET")))return td, nil
}func CreateAuth(userid uint64, td *TokenDetails) error {at := time.Unix(td.AtExpires, 0)rt := time.Unix(td.RtExpires, 0)now := time.Now()// 存儲Access TokenerrAccess := client.Set(td.AccessUuid, strconv.Itoa(int(userid)), at.Sub(now)).Err()if errAccess != nil {return errAccess}// 存儲Refresh TokenerrRefresh := client.Set(td.RefreshUuid, strconv.Itoa(int(userid)), rt.Sub(now)).Err()if errRefresh != nil {return errRefresh}return nil
}
3. 登錄接口實現
func Login(c *gin.Context) {var user Userif err := c.ShouldBindJSON(&user); err != nil {c.JSON(http.StatusUnprocessableEntity, "Invalid json provided")return}// 驗證用戶憑據// ...// 生成Tokentd, err := CreateToken(user.ID)if err != nil {c.JSON(http.StatusUnprocessableEntity, err.Error())return}// 存儲TokensaveErr := CreateAuth(user.ID, td)if saveErr != nil {c.JSON(http.StatusUnprocessableEntity, saveErr.Error())return}tokens := map[string]string{"access_token": td.AccessToken,"refresh_token": td.RefreshToken,}c.JSON(http.StatusOK, tokens)
}
4. Token刷新機制
func Refresh(c *gin.Context) {mapToken := map[string]string{}if err := c.ShouldBindJSON(&mapToken); err != nil {c.JSON(http.StatusUnprocessableEntity, err.Error())return}refreshToken := mapToken["refresh_token"]// 驗證Refresh Tokentoken, err := jwt.Parse(refreshToken, func(token *jwt.Token) (interface{}, error) {if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])}return []byte(os.Getenv("REFRESH_SECRET")), nil})if err != nil {c.JSON(http.StatusUnauthorized, "Refresh token expired")return}// 檢查Token是否有效if _, ok := token.Claims.(jwt.Claims); !ok && !token.Valid {c.JSON(http.StatusUnauthorized, err)return}// 提取claimsclaims, ok := token.Claims.(jwt.MapClaims)if ok && token.Valid {refreshUuid, ok := claims["refresh_uuid"].(string)if !ok {c.JSON(http.StatusUnprocessableEntity, err)return}userId, err := strconv.ParseUint(fmt.Sprintf("%.f", claims["user_id"]), 10, 64)if err != nil {c.JSON(http.StatusUnprocessableEntity, "Error occurred")return}// 刪除舊的Refresh Tokendeleted, delErr := DeleteAuth(refreshUuid)if delErr != nil || deleted == 0 {c.JSON(http.StatusUnauthorized, "unauthorized")return}// 創建新的Token對ts, createErr := CreateToken(userId)if createErr != nil {c.JSON(http.StatusForbidden, createErr.Error())return}// 保存新的TokensaveErr := CreateAuth(userId, ts)if saveErr != nil {c.JSON(http.StatusForbidden, saveErr.Error())return}tokens := map[string]string{"access_token": ts.AccessToken,"refresh_token": ts.RefreshToken,}c.JSON(http.StatusCreated, tokens)} else {c.JSON(http.StatusUnauthorized, "refresh expired")}
}
5. 中間件實現Token驗證
func TokenAuthMiddleware() gin.HandlerFunc {return func(c *gin.Context) {err := TokenValid(c.Request)if err != nil {c.JSON(http.StatusUnauthorized, err.Error())c.Abort()return}c.Next()}
}func TokenValid(r *http.Request) error {token, err := VerifyToken(r)if err != nil {return err}if _, ok := token.Claims.(jwt.Claims); !ok && !token.Valid {return err}return nil
}func VerifyToken(r *http.Request) (*jwt.Token, error) {tokenString := ExtractToken(r)token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])}return []byte(os.Getenv("ACCESS_SECRET")), nil})if err != nil {return nil, err}return token, nil
}func ExtractToken(r *http.Request) string {bearToken := r.Header.Get("Authorization")strArr := strings.Split(bearToken, " ")if len(strArr) == 2 {return strArr[1]}return ""
}
完整流程
- 用戶登錄:提供用戶名密碼,服務端驗證后返回Access Token和Refresh Token
- 訪問受保護資源:客戶端在請求頭中攜帶Access Token
- Access Token過期:服務端返回401錯誤
- 刷新Token:客戶端使用Refresh Token請求新的Token對
- 繼續訪問:使用新的Access Token訪問資源
總結
通過Go語言實現雙Token認證機制,我們能夠構建更安全的身份認證系統。這種機制在保證安全性的同時,也提供了良好的用戶體驗。實際應用中,可以根據業務需求調整Token的有效期和實現細節。
希望這篇文章對你理解和使用雙Token認證有所幫助!