参考 https://juejin.cn/post/7342766597243207715?searchId=20240903160011693AB9B562C29F25EA11
http://ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html

JWT 介紹

JWT 即 JSON web Token ,用於在網絡應用環境中安全地傳遞聲明claims
JWT 是一种紧凑且自包含的方式,用于作为 JSON 对象在各方之间安全地传输信息。由于其信息是经过数字签名的,所以可以确保发送的数据在传输过程中未被篡改。

JWT 組成結構

JWT 由三个部分组成,它们之间用 . 分隔,格式如下:Header.Payload.Signature、

  1. Header: Header部分用於描述該JWT的基本信息,比如其類型和所使用的算法

  2. Payload(負載): Payload部分包含所傳遞的聲明,聲明是關於實體和其他數據的語句。聲明能分爲三種類型:
    > 注冊聲明
    > 公共聲明
    > 私有聲明
    >

  3. Signature(簽名): 为了防止数据篡改,将头部和负载的信息进行一定算法处理,加上一个密钥,最后生成签名。如果使用的是 HMAC SHA256 算法,那么签名就是将编码后的头部、编码后的负载拼接起来,通过密钥进行HMAC SHA256 运算后的结果。

Signature 部分是对前两部分的签名,防止数据篡改。

首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。

HMACSHA256(
base64UrlEncode(header) + “.” +
base64UrlEncode(payload),
secret)
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。

實際使用

安裝go-jwt

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

创建 Token(JWT) 对象

生成 JWT 字符串首先需要创建 Token 对象(代表着一个 JWT)。因此我们需要先了解如何创建 Token 对象。

jwt 库主要通过两个函数来创建 Token 对象:NewWithClaims 和 New。

jwt.NewWithClaims 函数用于创建一个 Token 对象,该函数允许指定一个签名方法和一组声明claims)以及可变参数 TokenOption。下面是该函数的签名:

我們看源碼,可以發現它接受method->加密算法,claims->負載中的聲明部分,opts是TokenOption,返回一個Token對象的指針,這個函數給Token賦值了Method,Header也就是jwt的頭部,還有Claims負載中的聲明部分。

// Token represents a JWT Token.  Different fields will be used depending on
// whether you're creating or parsing/verifying a token.
type Token struct {
	Raw       string                 // Raw contains the raw token.  Populated when you [Parse] a token
	Method    SigningMethod          // Method is the signing method used or to be used
	Header    map[string]interface{} // Header is the first segment of the token in decoded form
	Claims    Claims                 // Claims is the second segment of the token in decoded form
	Signature []byte                 // Signature is the third segment of the token in decoded form.  Populated when you Parse a token
	Valid     bool                   // Valid specifies if the token is valid.  Populated when you Parse/Verify a token
}


以上為Token對象

我們再研究一下TokenOptions是什麽東西

// TokenOption is a reserved type, which provides some forward compatibility,
// if we ever want to introduce token creation-related options.
type TokenOption func(*Token)

這段註解說明了 TokenOption 是一個保留的類型,其主要目的是提供前向相容性。如果將來在你的系統中需要引入與 token 相關的選項或配置,這個類型可以幫助你更方便地進行擴展或修改。

換句話說,現在即使 TokenOption 可能沒有具體的用途或屬性,它仍然被保留在系統中,以便未來添加新功能時不會影響現有的代碼或結構。這是一種常見的設計模式,用來確保系統在將來需要添加新功能或更改現有功能時,可以保持相對穩定和靈活。

好,現在我們就用這個函數創建一個Token對象吧(這裏其實已經創建好Header.Payload 部分了

MapClaims 是 Go 言語中的一個類型,通常用於處理 JSON Web Token (JWT) 的聲明(claims)。在 jwt-go 庫中,MapClaims 是一個基於地圖(map[string]interface{})的結構,用來存儲和處理 JWT 的聲明。

// MapClaims is a claims type that uses the map[string]interface{} for JSON
// decoding. This is the default claims type if you don't supply one
type MapClaims map[string]interface{}
package main

import (
	"fmt"

	"github.com/golang-jwt/jwt/v5"
)

func main() {
	mapClaims := jwt.MapClaims{
		"iss": "meowrain",
		"sub": "jwt_learn",
		"aud": "public",
	}
	var token *jwt.Token = jwt.NewWithClaims(jwt.SigningMethodHS256, mapClaims)
	fmt.Println(token)
}



我們還能用jwt.New()函數創建Token對象

我們來看一下源碼,可以看到這個函數只接受一個簽名算法,一個TokenOption,然後調用了我們前面説的NewWithClaims函數,但是在clamis參數上傳遞了一個空的MapClaimsal

// New creates a new [Token] with the specified signing method and an empty map
// of claims. Additional options can be specified, but are currently unused.
func New(method SigningMethod, opts ...TokenOption) *Token {
	return NewWithClaims(method, MapClaims{}, opts...)
}
package main

import (
	"fmt"

	"github.com/golang-jwt/jwt/v5"
)

func main() {
	mapClaims := jwt.MapClaims{
		"iss": "meowrain",
		"sub": "jwt_learn",
		"aud": "public",
	}
	var token *jwt.Token = jwt.NewWithClaims(jwt.SigningMethodHS256, mapClaims)
	var token2 *jwt.Token = jwt.New(jwt.SigningMethodHS256)
	token2.Claims = mapClaims
	fmt.Println(token)
	fmt.Println(token2)
}

生成JWT字符串

通过使用 jwt.Token 对象的 SignedString 方法,我们能够对 JWT 对象进行序列化和签名处理,以生成最终的 token 字符串。该方法的签名如下:


// SignedString creates and returns a complete, signed JWT. The token is signed
// using the SigningMethod specified in the token. Please refer to
// https://golang-jwt.github.io/jwt/usage/signing_methods/#signing-methods-and-key-types
// for an overview of the different signing methods and their respective key
// types.
func (t *Token) SignedString(key interface{}) (string, error) {
	sstr, err := t.SigningString()
	if err != nil {
		return "", err
	}

	sig, err := t.Method.Sign(sstr, key)
	if err != nil {
		return "", err
	}

	return sstr + "." + t.EncodeSegment(sig), nil
}

这个函数接收一个key,类型为byte数组,我们传递进去

package main

import (
	"fmt"

	"github.com/golang-jwt/jwt/v5"
)

func main() {
	mapClaims := jwt.MapClaims{
		"iss": "meowrain",
		"sub": "jwt_learn",
		"aud": "public",
	}
	var token *jwt.Token = jwt.NewWithClaims(jwt.SigningMethodHS256, mapClaims)
	jwt_str, err := token.SignedString([]byte("chat"))
	if err != nil {
		panic(err)
	}
	fmt.Println(token)
	fmt.Println(jwt_str)
}


JWT解析

jwt 库主要通过两个函数来解析 jwt 字符串:Parse 和 ParseWithClaims。

// Parse parses, validates, verifies the signature and returns the parsed token.
// keyFunc will receive the parsed token and should return the cryptographic key
// for verifying the signature. The caller is strongly encouraged to set the
// WithValidMethods option to validate the 'alg' claim in the token matches the
// expected algorithm. For more details about the importance of validating the
// 'alg' claim, see
// https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/
func Parse(tokenString string, keyFunc Keyfunc, options ...ParserOption) (*Token, error) {
	return NewParser(options...).Parse(tokenString, keyFunc)
}

我們來看看keyFunc這個結構體組成


// Keyfunc will be used by the Parse methods as a callback function to supply
// the key for verification.  The function receives the parsed, but unverified
// Token.  This allows you to use properties in the Header of the token (such as
// `kid`) to identify which key to use.
//
// The returned interface{} may be a single key or a VerificationKeySet containing
// multiple keys.
type Keyfunc func(*Token) (interface{}, error)

可以看到這個函數是一個解析的回調函數,提供key來進行驗證


通用代碼

package main

import (
	"errors"
	"fmt"

	"github.com/golang-jwt/jwt/v5"
)

func GenerateJwt(claims *jwt.MapClaims, key string) (string, error) {
	var token *jwt.Token = jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	jwt_str, err := token.SignedString([]byte(key))
	if err != nil {
		return "", err
	}
	return jwt_str, nil
}
func ParseJwt(key []byte, jwtStr string, options ...jwt.ParserOption) (jwt.Claims, error) {
	token, err := jwt.Parse(jwtStr, func(token *jwt.Token) (interface{}, error) {
		return key, nil
	}, options...)
	if err != nil {
		return nil, err
	}
	if !token.Valid {
		return nil, errors.New("invalid token")
	}
	return token.Claims, nil
}
func main() {
	mapClaims := jwt.MapClaims{
		"iss": "meowrain",
		"sub": "jwt_learn",
		"aud": "public",
	}
	var token *jwt.Token = jwt.NewWithClaims(jwt.SigningMethodHS256, mapClaims)
	jwt_str, err := token.SignedString([]byte("chat"))
	if err != nil {
		panic(err)
	}
	fmt.Println(token)
	fmt.Println(jwt_str)
	claims, err := ParseJwt([]byte("chat"), jwt_str)
	if err != nil {
		panic(err)
	}
	fmt.Println(claims)
}


jwt.RegisterClaims和jwt.MapClaims

在 Go 的 JWT 库中,jwt.RegisteredClaimsjwt.MapClaims 都是用于处理 JWT(JSON Web Token)的声明(claims)的类型,但它们有一些区别:

  1. jwt.RegisteredClaims

    • 它是一个结构体,包含了一些 JWT 标准定义的注册声明(registered claims),如:
      • Issuer (iss): Token 的发行者。
      • Subject (sub): Token 的主题。
      • Audience (aud): Token 的接收者。
      • Expiration (exp): Token 的过期时间。
      • NotBefore (nbf): Token 在此时间之前无效。
      • IssuedAt (iat): Token 的签发时间。
      • ID (jti): Token 的唯一标识符。
    • 使用 RegisteredClaims 更方便处理标准声明,因为它提供了明确的字段和类型。

    示例

    claims := jwt.RegisteredClaims{
        Issuer:    "example.com",
        Subject:   "user_id",
        Audience:  []string{"example.com"},
        ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
    }
    
  2. jwt.MapClaims

    • 它是一个 map[string]interface{},可以存储任意键值对。你可以使用它来处理自定义声明或混合标准声明和自定义声明。
    • 灵活性更高,但在处理标准声明时需要自己解析这些字段。

    示例

    claims := jwt.MapClaims{
        "iss": "example.com",
        "sub": "user_id",
        "exp": time.Now().Add(time.Hour).Unix(),
        "customClaim": "custom_value",
    }
    

总结:

  • RegisteredClaims 适合处理标准 JWT 声明,提供了类型安全和易用性。
  • MapClaims 更灵活,适合处理自定义声明,但需要手动处理标准声明的键值。

你可以根据需求选择其中一种类型来处理 JWT。