跳过正文
  1. 文章/
  2. GoLang/
  3. 常用包/
  4. 第三方包/

9、jwt

·2877 字·6 分钟· loading · loading · ·
GoLang 常用包 第三方包
GradyYoung
作者
GradyYoung
第三方包 - 点击查看当前系列文章
§ 9、jwt 「 当前文章 」

JWT 概念
#

JSON Web TokenJWT)是一种开放标准(RFC 7519),用于在网络应用环境间安全地传递声明(claims)。JWT 是一种紧凑且自包含的方式,用于作为 JSON 对象在各方之间安全地传输信息。由于其信息是经过数字签名的,所以可以确保发送的数据在传输过程中未被篡改。

img

JWT 由三个部分组成,它们之间用 . 分隔,格式如下:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJQcm9ncmFtbWVyIiwiaXNzIjoi56iL5bqP5ZGY6ZmI5piO5YuHIiwic3ViIjoiY2hlbm1pbmd5b25nLmNuIn0.uRnH-rUb7lsZtQ11o8wXjIOJnIMBxszkvU1gY6hCGjo

  • Header(头部)Hedaer 部分用于描述该 JWT 的基本信息,比如其类型(通常是 JWT)以及所使用的签名算法(如 HMAC SHA256RSA)。
  • Payload(负载)Payload 部分包含所传递的声明。声明是关于实体(通常是用户)和其他数据的语句。声明可以分为三种类型:注册声明公共声明私有声明

注册声明:这些声明是预定义的,非必须使用的但被推荐使用。官方标准定义的注册声明有 7 个:

Claim(声明) 含义
iss(Issuer) 发行者,标识 JWT 的发行者。
sub(Subject) 主题,标识 JWT 的主题,通常指用户的唯一标识
aud(Audience) 观众,标识 JWT的接收者
exp(Expiration Time) 过期时间。标识 JWT 的过期时间,这个时间必须是将来的
nbf(Not Before) 不可用时间。在此时间之前,JWT 不应被接受处理
iat(Issued At) 发行时间,标识 JWT 的发行时间
jti(JWT ID) JWT 的唯一标识符,用于防止 JWT 被重放(即重复使用)

公共声明:可以由使用 JWT 的人自定义,但为了避免冲突,任何新定义的声明都应已在IANA JSON Web Token Registry中注册或者是一个 公共名称,其中包含了碰撞防抗性名称(Collision-Resistant Name)。

私有声明:发行和使用 JWT 的双方共同商定的声明,区别于 注册声明公共声明

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

golang-jwt
#

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

创建 Token(JWT) 对象
#

生成 JWT 字符串首先需要创建 Token 对象(代表着一个 JWT)。

jwt 库主要通过两个函数来创建 Token 对象:NewWithClaimsNew

NewWithClaims 函数
#

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

NewWithClaims(method SigningMethod, claims Claims, opts ...TokenOption) *Token
  • method:这是一个 SigningMethod 接口参数,用于指定 JWT 的签名算法。常用的签名算法有 SigningMethodHS256SigningMethodRS256等。这些算法分别代表不同的签名技术,如 HMACRSA
  • claims:这是一个 Claims 接口参数,它表示 JWT 的声明。在 jwt 库中,预定义了一些结构体来实现这个接口,例如 RegisteredClaimsMapClaims 等,通过指定 Claims 的实现作为参数,我们可以为JWT 添加声明信息,例如发行人(iss)、主题(sub)等。
  • opts:这是一个可变参数,允许传递零个或多个 TokenOption 类型参数。TokenOption 是一个函数,它接收一个 *Token,这样就可以在创建 Token 的时候对其进行进一步的配置。
package main

import (
	"fmt"
	"github.com/golang-jwt/jwt/v5"
)

func main() {
	mapClaims := jwt.MapClaims{
		"iss": "GradyYoung",
		"sub": "ygang.top",
		"aud": "Programmer",
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, mapClaims)
	fmt.Println(token != nil) // true
}

New 函数
#

jwt.New 函数用于创建一个 Token 对象,该函数允许指定一个签名方法和可变参数 TokenOption

func New(method SigningMethod, opts ...TokenOption) *Token {
    return NewWithClaims(method, MapClaims{}, opts...)
}

通过源码我们可以发现,该函数内部的实现通过调用 NewWithClaims 函数,并默认传入一个空的 MapClaims 对象,从而生成一个 Token 对象。

package main

import (
	"fmt"
	"github.com/golang-jwt/jwt/v5"
)

func main() {
	token := jwt.New(jwt.SigningMethodHS256)
	fmt.Println(token != nil) // true
}

生成 JWT 字符串
#

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

func (t *Token) SignedString(key interface{}) (string, error)
  • key:该参数是用于签名 token 的密钥。密钥的类型取决于使用的签名算法。
    • 如果使用 HMAC 算法(如 HS256HS384 等),key 应该是一个对称密钥(通常是 []byte 类型的密钥)。
    • 如果使用 RSAECDSA 签名算法(如 RS256ES256),key 应该是一个私钥 *rsa.PrivateKey*ecdsa.PrivateKey
  • 方法返回两个值:一个是成功签名后的 JWT 字符串,另一个是在签名过程中遇到的任何错误。
// https://github.com/chenmingyong0423/blog/blob/master/tutorial-code/go/jwt/generage-token/generate_token.go

package main

import (
	"crypto/rand"
	"fmt"
	"github.com/golang-jwt/jwt/v5"
)

func GenerateJwt(key any, method jwt.SigningMethod, claims jwt.Claims) (string, error) {
	token := jwt.NewWithClaims(method, claims)
	return token.SignedString(key)
}

func main() {
	jwtKey := make([]byte, 32) // 生成32字节(256位)的密钥
	if _, err := rand.Read(jwtKey); err != nil {
		panic(err)
	}
	jwtStr, err := GenerateJwt(jwtKey, jwt.SigningMethodHS256, jwt.MapClaims{
		"iss": "GradyYoung",
		"sub": "ygang.top",
		"aud": "Programmer",
	})
	if err != nil {
		panic(err)
	}
	fmt.Println(jwtStr)
}

解析 JWT 字符串
#

jwt 库主要通过两个函数来解析 jwt 字符串:ParseParseWithClaims

Parse 函数
#

Parse 函数用于解析 JWT 字符串,函数签名如下:

func Parse(tokenString string, keyFunc Keyfunc, options ...ParserOption) (*Token, error)
  • tokenString:要解析的 JWT 字符串。
  • keyFunc:这是一个回调函数,返回用于验证 JWT 签名的密钥。该函数签名为 func(*Token) (interface{}, error)。这种设计,有利于我们根据 token 对象的信息返回正确的密钥。例如我们可能有一个 keyMap 对象,类型为 map,该对象用于保存多个 key 的映射,通过 Token 对象的信息,拿到某个标识,就能通过 keyMap 获取到正确的密钥。
  • options:这是一个可变参数。允许传递零个或多个 ParserOption 类型参数。这些选项可以用来定制解析器的行为,如设置 exp 声明为必需的参数,否则解析失败。
// https://github.com/chenmingyong0423/blog/blob/master/tutorial-code/go/jwt/parse-token/parse.go

package main

import (
	"crypto/rand"
	"errors"
	"fmt"
	"github.com/golang-jwt/jwt/v5"
	"time"
)

func ParseJwt(key any, 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
	}
	// 校验 Claims 对象是否有效,基于 exp(过期时间),nbf(不早于),iat(签发时间)等进行判断(如果有这些声明的话)。
	if !token.Valid {
		return nil, errors.New("invalid token")
	}
	return token.Claims, nil
}

func main() {
	jwtKey := make([]byte, 32) // 生成32字节(256位)的密钥
	if _, err := rand.Read(jwtKey); err != nil {
		panic(err)
	}

	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
		"iss": "GradyYoung",
		"sub": "ygang.top",
		"aud": "Programmer",
		"exp": time.Now().Add(time.Second * 10).UnixMilli(),
	})
	jwtStr, err := token.SignedString(jwtKey)
	if err != nil {
		panic(err)
	}

	// 解析 jwt
	claims, err := ParseJwt(jwtKey, jwtStr, jwt.WithExpirationRequired())
	if err != nil {
		panic(err)
	}
	fmt.Println(claims)
}

ParseWithClaims 函数
#

ParseWithClaims 函数类似 Parse,函数签名如下:

func ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc, options ...ParserOption) (*Token, error)
  • tokenString:要解析的 JWT 字符串。
  • claims:这是一个 Claims 接口参数,用于接收解析 JWT 后的 claims 数据。
  • keyFunc:与 Parse 函数中的相同,用于提供验证签名所需的密钥。
  • options:与 Parse 函数中的相同,用来定制解析器的行为。
// https://github.com/chenmingyong0423/blog/blob/master/tutorial-code/go/jwt/parse-token/parse_with_claims.go

package main

import (
    "crypto/rand"
    "errors"
    "fmt"
    "github.com/golang-jwt/jwt/v5"
)

func ParseJwtWithClaims(key any, jwtStr string, options ...jwt.ParserOption) (jwt.Claims, error) {
    mc := jwt.MapClaims{}
    token, err := jwt.ParseWithClaims(jwtStr, mc, func(token *jwt.Token) (interface{}, error) {
       return key, nil
    }, options...)
    if err != nil {
       return nil, err
    }
    // 校验 Claims 对象是否有效,基于 exp(过期时间),nbf(不早于),iat(签发时间)等进行判断(如果有这些声明的话)。
    if !token.Valid {
       return nil, errors.New("invalid token")
    }
    return token.Claims, nil
}

func main() {
    jwtKey := make([]byte, 32) // 生成32字节(256位)的密钥
    if _, err := rand.Read(jwtKey); err != nil {
       panic(err)
    }

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
       "iss": "GradyYoung",
       "sub": "ygang.top",
       "aud": "Programmer",
    })
    jwtStr, err := token.SignedString(jwtKey)
    if err != nil {
       panic(err)
    }

    // 解析 jwt
    claims, err := ParseJwtWithClaims(jwtKey, jwtStr)
    if err != nil {
       panic(err)
    }
    fmt.Println(claims) 
}

JWT 工具
#

package utils

import (
	"errors"
	"github.com/golang-jwt/jwt/v5"
	"time"
)

// Claims 自定义 JWT 声明
type Claims struct {
	UserId uint `json:"userId"`
	jwt.RegisteredClaims
}

// GenerateToken 生成 JWT 令牌
func GenerateToken(userId uint, secret string, expirySecond int) (string, error) {
	// 设置过期时间
	now := time.Now()
	expirationTime := now.Add(time.Duration(expirySecond) * time.Second)
	// 创建声明
	claims := &Claims{
		UserId: userId,
		RegisteredClaims: jwt.RegisteredClaims{
			ExpiresAt: jwt.NewNumericDate(expirationTime),
			IssuedAt:  jwt.NewNumericDate(now),
		},
	}
	// 创建令牌
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	// 签名令牌
	signedString, err := token.SignedString([]byte(secret))
	if err != nil {
		return "", err
	}
	return signedString, nil
}

// ValidateToken 验证令牌
func ValidateToken(tokenString, secret string) (*Claims, error) {
	token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
		return []byte(secret), nil
	})
	if err != nil {
		return nil, err
	}
	if !token.Valid {
		return nil, errors.New("令牌无效!")
	}
	claims, ok := token.Claims.(*Claims)
	if !ok {
		return nil, errors.New("无法获取令牌声明!")
	}
	return claims, nil
}
第三方包 - 点击查看当前系列文章
§ 9、jwt 「 当前文章 」