golang gin后端開發框架(四):JWT和PASETO校驗中間件


1. Token-based Authentication

在這種驗證機制中,用戶第一次登錄需要POST自己的用戶名和密碼,在服務器端檢驗用戶名和密碼正確之后,就可以簽署一個令牌,並將其返回給客戶端

在此之后,客戶端就可以用這個access_token來訪問服務器上的資源,服務器只會驗證該令牌是否有效

同時,access_token有一定的生命周期,在這個周期內,客戶端都可以通過這個token來訪問服務器的資源

 

2. JWT

 JWT -- JSON Web Token

 

2.1 JWT簡介

JWT是一個base64編碼的字符串,主要由三部分組成:

  • header
  • payload
  • verify signature

 

其中header和payload是base64編碼的,而沒有加密,這意味着我們可以編碼或者解碼任意的payload,但是最后的藍色部分,也就是JWT簽名,保證了只有服務器有私鑰來簽署這個token

JWT提供了很多簽名算法,可以分為以下幾類:

  • 對稱秘鑰加密算法:適用於共享秘鑰的場景,本地,典型的算法有:HS256、HS384、HS512
  • 非對稱加密算法:私鑰對token簽名,公鑰驗證token,可以提供第三方服務,典型的算法有:RS256、PS256、ES256等

 

JWT的問題是什么?

(1)不安全的加密算法

  JWT給開發者提供了很多的加密算法選擇,其中就包括了已知的易受攻擊的算法

 (2)在header中包含了簽名算法的種類

  攻擊者只需要將header中的alg字段設置為none就可以繞過簽名驗證過程

  在知道服務器使用非對稱加密算法的情況下,修改alg為一個對稱加密算法

 

2.2 在golang中實現JWT

首先我們定義一個token maker的接口,在之后會使用PASETO和JWT來實現這個接口

Maker接口包括了兩個方法,分別是創建token和驗證token:

type Maker interface {
	// CreateToken 創建一個token
	CreateToken(username string, duration time.Duration) (string, error)
	// VerifyToken 驗證token
	VerifyToken(token string) (*Payload, error)
}

  

現在定義token的payload結構體,其中應該包含一些我們需要的字段,一般意義上就是用戶名、創建時間、過期時間、tokenID這幾個信息:

type Payload struct {
	ID        uuid.UUID `json:"id" `
	Username  string    `json:"username" `
	IssuedAt  time.Time `json:"issued_at" `
	ExpiredAt time.Time `json:"expired_at" `
}

 

然后對外提供一個創建payload的函數:

func NewPayload(username string, duration time.Duration) (*Payload, error) {
	tokenID, err := uuid.NewRandom()
	if err != nil {
		return nil, err
	}

	payload := &Payload{
		ID:        tokenID,
		Username:  username,
		IssuedAt:  time.Now(),
		ExpiredAt: time.Now().Add(duration),
	}
	return payload, nil
}

  

現在我們就可以開始實現JWT token的代碼了,其需要實現Maker接口定義的兩個方法

func (maker *JWTMaker) CreateToken(username string, duration time.Duration) (string, error) {
	payload, err := NewPayload(username, duration)
	if err != nil {
		return "", err
	}

	jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, payload)
	return jwtToken.SignedString([]byte(maker.secretKey))
}

 

值得注意的是,在jwt.NewWithClaims()方法中,我們傳入payload時會報錯,仔細看提示會發現jwt需要我們定義的payload結構體提供一個驗證功能,就是一個 func(payload *Payload) Valid() error 簽名的函數

我們就可以做一個簡單的過期時間驗證:

func (payload *Payload) Valid() error {
    if time.Now().After(payload.ExpiredAt) {
        return ErrExpiredToken
    }
    return nil
}

 

同樣,我們再去實現驗證token的方法:

func (maker *JWTMaker) VerifyToken(token string) (*Payload, error) {
	keyFunc := func(token *jwt.Token) (interface{}, error) {
		_, ok := token.Method.(*jwt.SigningMethodHMAC)
		if !ok {
			return nil, ErrInvalidToken
		}
		return []byte(maker.secretKey), nil
	}
	jwtToken, err := jwt.ParseWithClaims(token, &Payload{}, keyFunc)
	if err != nil {
		verr, ok := err.(*jwt.ValidationError)
		if ok && errors.Is(verr.Inner, ErrExpiredToken) {
			return nil, ErrExpiredToken
		}
		return nil, ErrInvalidToken
	}

	payload, ok := jwtToken.Claims.(*Payload)
	if !ok {
		return nil, ErrInvalidToken
	}
	return payload, nil
}

 

 在 jwtToken, err := jwt.ParseWithClaims(token, &Payload{}, keyFunc) 中

keyFunc需要我們自己實現,其作用是驗證header中的簽名算法是否合法,防止一些瑣碎的攻擊

同樣err在jwt包內部是被隱藏的,對於驗證失敗的令牌有兩種情況:令牌過期或者令牌不合法

所以我們需要做一次類型斷言,找出具體的錯誤來做返回

 

3. PASETO

PASETO -- Platform-Agnostic SEcurity TOkens

 

3.1 PASETO簡介

每一個版本的PASETO都包含了強大的加密套件,選擇對應的加密算法只需要選擇PASETO版本即可

最多只能有兩個版本同時處於活躍狀態

 

 

相比於JWT,PASETO所做的改變在於:

  • 不會向用戶開放所有的加密算法
  • header中不再含有alg字段,也不會有none算法
  • payload使用加密算法,而不是簡單的編碼

 

PASETO的令牌結構:

 

 

 

3.2 在golang中實現PASETO

PASETO的實現要比JWT簡單一些,我們同樣還是使用對稱加密算法來實現,首先是創建token的方法:

func (maker *PasetoMaker) CreateToken(username string, duration time.Duration) (string, error) {
	payload, err := NewPayload(username, duration)
	if err != nil {
		return "", err
	}
	return maker.paseto.Encrypt(maker.symmetricKey, payload, nil)
}

  

然后是驗證token:

func (maker *PasetoMaker) VerifyToken(token string) (*Payload, error) {
	payload := &Payload{}

	if err := maker.paseto.Decrypt(token, maker.symmetricKey, payload, nil); err != nil {
		return nil, ErrInvalidToken
	}
	if err := payload.Valid(); err != nil {
		return nil, err
	}

	return payload, nil
}

  

至此我們就完成了PASETO對稱加密的token

 

4. 實現token驗證中間件

首先客戶端需要提供登錄信息,包括了用戶名和密碼。然后服務器創建一個token返回給客戶端,用於之后的身份驗證

const (
	authorizationHeaderKey  = "authorization"
	authorizationTypeBearer = "bearer"
	authorizationPayloadKey = "authorization_payload"
)

func authMiddleware(tokenMaker Maker) gin.HandlerFunc {
	return func(ctx *gin.Context) {
		authorizationHeader := ctx.GetHeader(authorizationHeaderKey)
		if len(authorizationHeader) == 0 {
			err := errors.New("authorization header is not provide")
			ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err})
			return
		}

		fields := strings.Fields(authorizationHeader)
		if len(fields) < 2 {
			err := errors.New("invalid authorization header format")
			ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err})
			return
		}

		authorizationType := strings.ToLower(fields[0])
		if authorizationType != authorizationTypeBearer {
			err := fmt.Errorf("unsupported authorization type %s", authorizationType)
			ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err})
			return
		}

		accessToken := fields[1]
		payload, err := tokenMaker.VerifyToken(accessToken)
		if err != nil {
			ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err})
			return
		}

		ctx.Set(authorizationPayloadKey, payload)
		ctx.Next()
	}
}

  

然后我們可以將需要授權的api做一個路由組,使用這個中間件

同時我們在授權階段可以簡單的使用一個ctx.MustGet()方法來取得token中的payload,里面包含有用戶名的驗證信息,這樣就可以保證用戶只可以訪問自己的相關內容


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM