三十的博客

学习 Go 中 Jwt 的基本使用

发布时间
阅读量 加载中...

Jwt 是什么

Jwt 是一种用于身份验证和授权的开放标准(RFC 7519)。它通常用于在不同系统之间传递声明(claims),这些声明可以是用户信息、权限等。

推荐阅读:

阮一峰 Jwt 入门教程 | Jwt 官方文档

安装 Jwt

go-jwt 官方仓库
  1. 安装 Jwt 包
bash
go get -u github.com/golang-jwt/jwt/v5
  1. 导入 Jwt 包
go
import "github.com/golang-jwt/jwt/v5"

默认 claims

默认 claims 包含了一些标准的声明,如 audexpiss 等。

go
package main

import (
	"errors"
	"fmt"
	"time"
)

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

// 用于签名的字符串
var mySigningKey = []byte("sanshi")

var (
	TokenValid            = errors.New("未知错误")
	TokenExpired          = errors.New("token已过期")
	TokenNotValidYet      = errors.New("token尚未激活")
	TokenMalformed        = errors.New("这不是一个token")
	TokenSignatureInvalid = errors.New("无效签名")
	TokenInvalid          = errors.New("无法处理此token")
)

// getToken 使用默认声明创建jwt
func getToken() (string, error) {
	// 创建 Claims
	claims := &jwt.RegisteredClaims{
		Audience:  jwt.ClaimStrings{"sanshi-blog"},                    // 受众
		ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24)), // 过期时间
		Issuer:    "sanshi",                                           // 签发人
	}
	// 生成token对象
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	// 生成签名字符串
	return token.SignedString(mySigningKey)
}

// parseToken 解析 token
func parseToken(tokenString string) (jwt.Claims, error) {

	token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
		return mySigningKey, nil
	})
	if err != nil {

		switch {
		case errors.Is(err, jwt.ErrTokenExpired):
			return nil, TokenExpired
		case errors.Is(err, jwt.ErrTokenMalformed):
			return nil, TokenMalformed
		case errors.Is(err, jwt.ErrTokenSignatureInvalid):
			return nil, TokenSignatureInvalid
		case errors.Is(err, jwt.ErrTokenNotValidYet):
			return nil, TokenNotValidYet
		default:
			return nil, TokenInvalid
		}
	}

	if claims, ok := token.Claims.(jwt.Claims); ok && token.Valid {
		return claims, nil
	}
	return nil, TokenValid
}

func main() {
	token, _ := getToken()

	fmt.Println(token)

	claims, err := parseToken(token)
	if err != nil {
		panic(err)
	}

	fmt.Printf("%+v", claims)
}

自定义 claims

当你需要在 token 中存储自定义的信息时,你可以自定义 claims。

go
package main

import (
	"errors"
	"fmt"
	"time"
)

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

var (
	TokenValid            = errors.New("未知错误")
	TokenExpired          = errors.New("token已过期")
	TokenNotValidYet      = errors.New("token尚未激活")
	TokenMalformed        = errors.New("这不是一个token")
	TokenSignatureInvalid = errors.New("无效签名")
	TokenInvalid          = errors.New("无法处理此token")
)

type CustomClaims struct {
	BaseClaims
	BufferTime int64 // 缓存时间可以用于刷新 token 判断使用
	jwt.RegisteredClaims
}

type BaseClaims struct {
	ID       uint
	Username string
	RoleId   uint
}

type JWT struct {
	SigningKey []byte
}

func NewJWT() *JWT {
	return &JWT{
		[]byte("sanshi"),
	}
}

// GetToken 使用默认声明创建jwt
func (j *JWT) GetToken() (string, error) {
	// 创建 Claims
	claims := CustomClaims{
		BufferTime: int64(3600 * time.Second),
		BaseClaims: BaseClaims{
			ID:       1,
			Username: "wangLei",
			RoleId:   1,
		},
		RegisteredClaims: jwt.RegisteredClaims{
			Audience:  jwt.ClaimStrings{"sanshi-blog"},                    // 受众
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24)), // 过期时间
			Issuer:    "sanshi",                                           // 签发人
		},
	}
	// 生成 token
	return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(j.SigningKey)
}

func (j *JWT) keyFunc(token *jwt.Token) (any, error) {
	return j.SigningKey, nil
}

// ParseToken 解析 token
func (j *JWT) ParseToken(tokenString string) (jwt.Claims, error) {

	token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, j.keyFunc)

	if err != nil {
		switch {
		case errors.Is(err, jwt.ErrTokenExpired):
			return nil, TokenExpired
		case errors.Is(err, jwt.ErrTokenMalformed):
			return nil, TokenMalformed
		case errors.Is(err, jwt.ErrTokenSignatureInvalid):
			return nil, TokenSignatureInvalid
		case errors.Is(err, jwt.ErrTokenNotValidYet):
			return nil, TokenNotValidYet
		default:
			return nil, TokenInvalid
		}
	}

	if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
		return claims, nil
	}
	return nil, TokenValid
}

func main() {
	j := NewJWT()
	token, _ := j.GetToken()

	fmt.Println(token)

	claims, err := j.ParseToken(token)
	if err != nil {
		panic(err)
	}

	fmt.Printf("%+v", claims)
}

无感刷新 token

refresh_token

通过提供一个 refresh_token 来刷新 token。

go
package main

import (
	"errors"
	"fmt"
	"time"
)

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

var (
	TokenValid            = errors.New("未知错误")
	TokenExpired          = errors.New("token已过期")
	TokenNotValidYet      = errors.New("token尚未激活")
	TokenMalformed        = errors.New("这不是一个token")
	TokenSignatureInvalid = errors.New("无效签名")
	TokenInvalid          = errors.New("无法处理此token")

	tokenExpired   = 30 * 60 * time.Second
	refreshExpired = 40 * 60 * time.Second
)

type CustomClaims struct {
	BaseClaims
	jwt.RegisteredClaims
}

type BaseClaims struct {
	ID       uint
	Username string
	RoleId   uint
}

type JWT struct {
	SigningKey []byte
}

func NewJWT() *JWT {
	return &JWT{
		[]byte("sanshi"),
	}
}

// GetToken 使用默认声明创建jwt
func (j *JWT) GetToken() (string, string, error) {
	// 创建 Claims
	claims := CustomClaims{
		BaseClaims: BaseClaims{
			ID:       1,
			Username: "wangLei",
			RoleId:   1,
		},
		RegisteredClaims: jwt.RegisteredClaims{
			Audience:  jwt.ClaimStrings{"sanshi-blog"},                  // 受众
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(tokenExpired)), // 过期时间
			Issuer:    "sanshi",                                         // 签发人
		},
	}
	// 生成 token
	aToken, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(j.SigningKey)
	if err != nil {
		return "", "", err
	}

	// 生成 refresh_token
	rToken, err := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{
		Audience:  jwt.ClaimStrings{"wangLei"},
		ExpiresAt: jwt.NewNumericDate(time.Now().Add(refreshExpired)),
		Issuer:    "wangLei",
	}).SignedString(j.SigningKey)

	return aToken, rToken, err
}

// ParseToken 解析 token
func (j *JWT) ParseToken(tokenString string) (jwt.Claims, error) {

	token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, j.keyFunc)

	if err != nil {
		switch {
		case errors.Is(err, jwt.ErrTokenExpired):
			return nil, TokenExpired
		case errors.Is(err, jwt.ErrTokenMalformed):
			return nil, TokenMalformed
		case errors.Is(err, jwt.ErrTokenSignatureInvalid):
			return nil, TokenSignatureInvalid
		case errors.Is(err, jwt.ErrTokenNotValidYet):
			return nil, TokenNotValidYet
		default:
			return nil, TokenInvalid
		}
	}

	if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
		return claims, nil
	}
	return nil, TokenValid
}

func (j *JWT) keyFunc(token *jwt.Token) (any, error) {
	return j.SigningKey, nil
}

func (j *JWT) RefreshToken(aToken, rToken string) (newAToken, newRToken string, err error) {
	// refresh 无效直接返回
	if _, err = jwt.Parse(rToken, j.keyFunc); err != nil {
		return
	}

	// 从旧的 access_token 中解析出 claims 数据
	var claims CustomClaims
	_, err = jwt.ParseWithClaims(aToken, &claims, j.keyFunc)

	// 当 access_token 是过期错误,并且 refresh_token 没有过期 则创建一个新的 access_token
	if errors.Is(err, jwt.ErrTokenExpired) {
		// 同步再次刷新 refresh_token 可以用来实现永久登录

		// 当 refresh_token 过期要求用户重新登录
		newAToken, _, err = j.GetToken()
		if err != nil {
			return
		}

		return newAToken, rToken, nil
	}
	return
}

func main() {
	j := NewJWT()
	aToken, rToken, _ := j.GetToken()

	fmt.Println(aToken, rToken)

	claims, err := j.ParseToken(aToken)
	if err != nil {
		panic(err)
	}

	fmt.Printf("%+v \n", claims)

	claims, err = j.ParseToken(rToken)
	if err != nil {
		panic(err)
	}

	fmt.Printf("%+v \n", claims)
}

缓冲时间

当 token 快要过期并且在即将过期的一段时间内请求了接口,自动将 token 续期。

go
package main

import (
	"errors"
	"fmt"
	"golang.org/x/sync/singleflight"
	"time"
)

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

var (
	TokenValid            = errors.New("未知错误")
	TokenExpired          = errors.New("token已过期")
	TokenNotValidYet      = errors.New("token尚未激活")
	TokenMalformed        = errors.New("这不是一个token")
	TokenSignatureInvalid = errors.New("无效签名")
	TokenInvalid          = errors.New("无法处理此token")

	ConcurrencyControl = &singleflight.Group{}

	tokenExpired     = 30 * 60 * time.Second
	bufferTime       = 40 * 60 * time.Second
	testTokenExpired = 60 * 60 * time.Second
)

type CustomClaims struct {
	BaseClaims
	BufferTime int64 // 缓存时间可以用于刷新 token 判断使用
	jwt.RegisteredClaims
}

type BaseClaims struct {
	ID       uint
	Username string
	RoleId   uint
}

type JWT struct {
	SigningKey []byte
}

func NewJWT() *JWT {
	return &JWT{
		[]byte("sanshi"),
	}
}

// GetToken 使用默认声明创建jwt
func (j *JWT) GetToken(claims *CustomClaims) (string, error) {
	// 生成 token
	return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(j.SigningKey)
}

func (j *JWT) keyFunc(token *jwt.Token) (any, error) {
	return j.SigningKey, nil
}

// ParseToken 解析 token
func (j *JWT) ParseToken(tokenString string) (*CustomClaims, error) {

	token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, j.keyFunc)

	if err != nil {
		switch {
		case errors.Is(err, jwt.ErrTokenExpired):
			return nil, TokenExpired
		case errors.Is(err, jwt.ErrTokenMalformed):
			return nil, TokenMalformed
		case errors.Is(err, jwt.ErrTokenSignatureInvalid):
			return nil, TokenSignatureInvalid
		case errors.Is(err, jwt.ErrTokenNotValidYet):
			return nil, TokenNotValidYet
		default:
			return nil, TokenInvalid
		}
	}

	if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
		return claims, nil
	}
	return nil, TokenValid
}

// RefreshToken 刷新 token
func (j *JWT) RefreshToken(token string) (string, error) {
	customClaims, err := j.ParseToken(token)
	if err != nil {
		return "", TokenInvalid
	}

	if customClaims.ExpiresAt.Unix()-time.Now().Unix() < customClaims.BufferTime {
		customClaims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(testTokenExpired))
		newToken, _ := j.CreateTokenByOldToken(token, customClaims)
		return newToken, nil
	}
	return token, nil
}

// CreateTokenByOldToken 重新生成 token
func (j *JWT) CreateTokenByOldToken(token string, c *CustomClaims) (string, error) {
	v, err, _ := ConcurrencyControl.Do("JWT:"+token, func() (interface{}, error) {
		return j.GetToken(c)
	})
	return v.(string), err
}

func main() {
	j := NewJWT()

	c := CustomClaims{
		BufferTime: int64(bufferTime),
		BaseClaims: BaseClaims{
			ID:       1,
			Username: "wangLei",
			RoleId:   1,
		},
		RegisteredClaims: jwt.RegisteredClaims{
			Audience:  jwt.ClaimStrings{"sanshi-blog"},                  // 受众
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(tokenExpired)), // 过期时间
			Issuer:    "sanshi",                                         // 签发人
		},
	}

	token, _ := j.GetToken(&c)

	fmt.Println(token)

	claims, err := j.ParseToken(token)
	if err != nil {
		panic(err)
	}

	fmt.Printf("%+v \n", claims)

	token, _ = j.RefreshToken(token)

	fmt.Println(token)

	claims, err = j.ParseToken(token)
	if err != nil {
		panic(err)
	}

	fmt.Printf("%+v \n", claims)
}
#Jwt #Golang