Golang 一日一库之jwt-go

本文详细介绍了JSONWebToken(JWT)的概念、优势与劣势,以及如何在Go语言中使用dgrijalva/jwt-go和golang-jwt库生成和解析JWT。文章通过示例展示了JWT的构成,包括Header、Payload和Signature,并讨论了如何处理JWT的过期和续签问题。

本文地址 https://www.cnblogs.com/zichliang/p/17303759.html

github地址:GitHub - dgrijalva/jwt-go: ARCHIVE - Golang implementation of JSON Web Tokens (JWT). This project is now maintained at:

何为 jwt token?

什么是JSON Web Token?
JSON Web Token(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间以JSON方式安全地传输信息。由于此信息是经过数字签名的,因此可以被验证和信任。可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对对JWT进行签名。
直白的讲jwt就是一种用户认证(区别于session、cookie)的解决方案。

jwt的优势与劣势

优点:

  1. 多语言支持
  2. 通用性好,不存在跨域问题
  3. 数据签名相对安全。
  4. 不需要服务端集中维护token信息,便于扩展。

缺点:
1、用户无法主动登出,只要token在有效期内就有效。这里可以考虑redis设置同token有效期一直的黑名单解决此问题。

2、token过了有效期,无法续签问题。可以考虑通过判断旧的token什么时候到期,过期的时候刷新token续签接口产生新token代替旧token

JWT的构成

Header是头部
Jwt的头部承载两部分信息:
声明类型,这里是jwt
声明加密的算法 通常直接使用 HMAC SHA256

Playload(载荷又称为Claim)

playload可以填充两种类型数据
简单来说就是 比如用户名、过期时间等,

  1. 标准中注册的声明

iss: 签发者
sub: 面向的用户
aud: 接收方
exp: 过期时间
nbf: 生效时间
iat: 签发时间
jti: 唯一身份标识

  1. 自定义声明

Signature(签名)

是由header、payload 和你自己维护的一个 secret 经过加密得来的
签名的算法:

HMACSHA256(
    base64UrlEncode(header) + "." +
    base64UrlEncode(payload),
    secret
)

golang-jwt/jwt

安装

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

这里注意 **最新版是V5 但是我们使用的V4, V5 的用法 也一样 不过需要实现Claims的接口方法 一共有六个左右。并且更加严谨了 **

注册声明结构体

注册声明是JWT声明集的结构化版本,仅限于注册声明名称

type JwtCustomClaims struct {
	ID   int
	Name string
	jwt.RegisteredClaims
}

生成Token

首先需要初始化Clamins 其次在初始化结构体中注册并且设置好过期时间 主题 以及生成时间等等。。
然后会发现 jwt.RegisteredClaims
在这个方法中 还需要实现Claims接口 还需要定义几个方法
如上图所示
然后我们使用使用HS256 的签名加密方法使用指定的签名方法和声明创建一个新的[Token]
代码如下

// 本文地址 https://www.cnblogs.com/zichliang/p/17303759.html
// GenerateToken 生成Token
func GenerateToken(id int, name string) (string, error) {
	// 初始化
	iJwtCustomClaims := JwtCustomClaims{
		ID:   id,
		Name: name,
		RegisteredClaims: jwt.RegisteredClaims{
			// 设置过期时间 在当前基础上 添加一个小时后 过期
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(viper.GetDuration("jwt.TokenExpire") * time.Millisecond)),
			// 颁发时间 也就是生成时间
			IssuedAt: jwt.NewNumericDate(time.Now()),
			//主题
			Subject: "Token",
		},
	}
	
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, iJwtCustomClaims)
	return token.SignedString(stSignKey)
}

还有一个小坑 这里的stsignKey 必须是byte字节的
所以我们在设置签名秘钥 必须要使用byte强转
像这个样子。

然后我们去执行
传入一个ID 和一个name

token, _ := utils.GenerateToken(1, "张三")
fmt.Println(token)


得到如下值eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJJRCI6MSwiTmFtZSI6IuW8oOS4iSIsIlJlZ2lzdGVyZWRDbGFpbXMiOnsic3ViIjoiVG9rZW4iLCJleHAiOjE2ODExODI2MDYsImlhdCI6MTY4MTE4MjYwNn19.AmOf60S2xby6GmlGgNo4Q5b01cRoAqXWhGorzxbJ2-Q

解析Token

JSON Web Tokens - jwt.io
在写代码之前,我们把上面的token丢到上面网站中解析一下
可以发现 有三部分被解析出来了

  1. Header 告诉我们用的是什么算法,类型是什么
  2. PayLoad 我们自定义的一些数据
  3. Signature 之后服务器解析做的签名验证

代码解析token

  1. 声明一个空的数据声明
  2. 调用 jwt.ParseWithClaims 方法
  3. 传入token 数据声明接口,
  4. 判断Token是否有效
  5. 返回token
// ParseToken 解析token
func ParseToken(tokenStr string) (JwtCustomClaims, error) {
	// 声明一个空的数据声明
	iJwtCustomClaims := JwtCustomClaims{}
	//ParseWithClaims是NewParser().ParseWithClaims()的快捷方式
	//第一个值是token ,
	//第二个值是我们之后需要把解析的数据放入的地方,
	//第三个值是Keyfunc将被Parse方法用作回调函数,以提供用于验证的键。函数接收已解析但未验证的令牌。
	token, err := jwt.ParseWithClaims(tokenStr, &iJwtCustomClaims, func(token *jwt.Token) (interface{}, error) {
		return stSignKey, nil
	})

	// 判断 是否为空 或者是否无效只要两边有一处是错误 就返回无效token
	if err != nil && !token.Valid {
		err = errors.New("invalid Token")
	}
	return iJwtCustomClaims, err
}

返回成功如下图所示

由于我们主动抛了个错,那我们如果手动传入错的token 看他是否会抛出错误提示呢?

jwtCustomClaim, err := utils.ParseToken(token + "12312323123")

结果:

答案是会。

完整代码

完整代码
package utils

import (
	"errors"
	"fmt"
	"github.com/golang-jwt/jwt/v4"
	"github.com/spf13/viper"
	"time"
)

// 把签发的秘钥 抛出来
var stSignKey = []byte(viper.GetString("jwt.SignKey"))

// JwtCustomClaims 注册声明是JWT声明集的结构化版本,仅限于注册声明名称
type JwtCustomClaims struct {
	ID               int
	Name             string
	RegisteredClaims jwt.RegisteredClaims
}

func (j JwtCustomClaims) Valid() error {
	return nil
}

// GenerateToken 生成Token
func GenerateToken(id int, name string) (string, error) {
	// 初始化
	iJwtCustomClaims := JwtCustomClaims{
		ID:   id,
		Name: name,
		RegisteredClaims: jwt.RegisteredClaims{
			// 设置过期时间 在当前基础上 添加一个小时后 过期
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(viper.GetDuration("jwt.TokenExpire") * time.Minute)),
			// 颁发时间 也就是生成时间
			IssuedAt: jwt.NewNumericDate(time.Now()),
			//主题
			Subject: "Token",
		},
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, iJwtCustomClaims)
	return token.SignedString(stSignKey)
}

// ParseToken 解析token
func ParseToken(tokenStr string) (JwtCustomClaims, error) {
	iJwtCustomClaims := JwtCustomClaims{}
	//ParseWithClaims是NewParser().ParseWithClaims()的快捷方式
	token, err := jwt.ParseWithClaims(tokenStr, &iJwtCustomClaims, func(token *jwt.Token) (interface{}, error) {
		return stSignKey, nil
	})

	if err == nil && !token.Valid {
		err = errors.New("invalid Token")
	}
	return iJwtCustomClaims, err
}

func IsTokenValid(tokenStr string) bool {
	_, err := ParseToken(tokenStr)
	fmt.Println(err)
	if err != nil {
		return false
	}
	return true
}

dgrijalva/jwt-go

安装

go get -u "github.com/dgrijalva/jwt-go"

生成JWT

这里需要传入用户名和密码
然后根据SHA256 去进行加密 从而吧payload生成token

// 本文地址 https://www.cnblogs.com/zichliang/p/17303759.html
func Macke(user *Userinfo) (token string, err error) {
	claims := jwt.MapClaims{ //创建一个自己的声明
		"name": user.Username,
		"pwd":  user.Password,
		"iss":  "lva",
		"nbf":  time.Now().Unix(),
		"exp":  time.Now().Add(time.Second * 4).Unix(),
		"iat":  time.Now().Unix(),
	}

	then := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

	token, err = then.SignedString([]byte("gettoken"))

	return
}

制定解析规则


在自己写的这个函数中 我们点进源码看返回值
解析方法使用此回调函数提供用于验证的键。函数接收已解析但未验证的令牌。
这允许您使用令牌Header中的属性(例如' kid ')来识别使用哪个键。

上述是源码的意思 而本人理解是制定一个类型规则然后去做解析。不然源码不知道你是制作token 还是解析token

func secret() jwt.Keyfunc {
	//按照这样的规则解析
	return func(t *jwt.Token) (interface{}, error) {
		return []byte("gettoken"), nil
	}
}

解析token

首先需要传入一个token,然后把解析规则传入
然后需要验证Token的正确性以及有效性。
如果二者都是没问题的
然后才能解析出 用户名和密码 或者是其他的一些值

// 解析token
func ParseToken(token string) (user *Userinfo, err error) {
	user = &Userinfo{}
	tokn, _ := jwt.Parse(token, secret())

	claim, ok := tokn.Claims.(jwt.MapClaims)
	if !ok {
		err = errors.New("解析错误")
		return
	}
	if !tokn.Valid {
		err = errors.New("令牌错误!")
		return
	}
	//fmt.Println(claim)
	user.Username = claim["name"].(string) //强行转换为string类型
	user.Password = claim["pwd"].(string)  //强行转换为string类型
	return
}

完整代码

完整代码
// 本文地址 https://www.cnblogs.com/zichliang/p/17303759.html
package main

import (
	"errors"
	"fmt"
	"github.com/dgrijalva/jwt-go"
	"time"
)

type Userinfo struct {
	Username string `json:"username"`
	Password string `json:"password"`
}

// Macke 生成jwt 需要传入 用户名和密码
func Macke(user *Userinfo) (token string, err error) {
	claims := jwt.MapClaims{ //创建一个自己的声明
		"name": user.Username,
		"pwd":  user.Password,
		"iss":  "lva",
		"nbf":  time.Now().Unix(),
		"exp":  time.Now().Add(time.Second * 4).Unix(),
		"iat":  time.Now().Unix(),
	}

	then := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

	token, err = then.SignedString([]byte("gettoken"))

	return
}

// secret 自己解析的秘钥
func secret() jwt.Keyfunc {
	//按照这样的规则解析
	return func(t *jwt.Token) (interface{}, error) {
		return []byte("gettoken"), nil
	}
}

// 解析token
func ParseToken(token string) (user *Userinfo, err error) {
	user = &Userinfo{}
	tokn, _ := jwt.Parse(token, secret())

	claim, ok := tokn.Claims.(jwt.MapClaims)
	if !ok {
		err = errors.New("解析错误")
		return
	}
	if !tokn.Valid {
		err = errors.New("令牌错误!")
		return
	}
	//fmt.Println(claim)
	user.Username = claim["name"].(string) //强行转换为string类型
	user.Password = claim["pwd"].(string)  //强行转换为string类型
	return
}

func main() {
	var use = Userinfo{"zic", "admin*123"}
	tkn, _ := Macke(&use)
	fmt.Println("_____", tkn)
	// time.Sleep(time.Second * 8)超过时间打印令牌错误
	user, err := ParseToken(tkn)
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(user.Username)
}

本文地址 https://www.cnblogs.com/zichliang/p/17303759.html

这里需要注意用户请求时带上token,服务器解析token后可以获得其中的用户信息,如果token有任何改动,都无法通过验证.

我想我們先整合下,先給我擴充後的整個完整目錄結構:my-app/ ├── README.md ├── Makefile ├── .gitignore ├── .editorconfig ├── .prettierrc ├── .eslintrc.cjs ├── .golangci.yml ├── .dockerignore ├── .vscode/ │ ├── extensions.json │ └── settings.json │ ├── contracts/ # 契約單真相(SSOT) │ ├── common.yaml # 全域元資料(servers、security、RBAC、components 共用) │ ├── domains/ │ │ ├── users.yaml # 使用者領域 schema/paths 片段 │ │ └── auth.yaml # 認證授權相關 schema/paths 片段 │ ├── endpoints/ │ │ ├── users.list.yaml # 端點片段(可拆分便於維護) │ │ └── users.detail.yaml │ └── openapi.base.yaml # OpenAPI 合成基底(版本/聯絡/License 等) │ ├── api/ # 由 contracts 合成的 OpenAPI(生成物) │ ├── openapi.yaml # 最終合成 OpenAPI │ └── snapshot/ │ └── openapi.2025-10-27.yaml # 每次 CI 產生快照(便於 diff 與回滾) │ ├── scripts/ # 生成/驗證/工具腳本 │ ├── contracts-to-openapi.mjs # 合併 contracts → api/openapi.yaml │ ├── gen-openapi.sh # 調用 oapi-codegen / openapi-typescript / zod wrap │ ├── gen-zod-from-openapi.mjs # 將 TS types 轉 zod schema(或直接嵌入) │ ├── validate-openapi.mjs # 基於 spectral 或 openapi-diff 驗證 │ ├── validate-artifact.mjs # 驗證 repo/data 與 schema(CI 用) │ ├── bump-version.mjs # 語義化版本管理 │ └── release-notes.mjs # 變更日誌生成 │ ├── repo/ # Artifact 倉庫(可被 Gateway/mock/e2e 使用) │ ├── api/ │ │ ├── index.json # 路由索引(method/path → flow 與 data 映射) │ │ └── flows/ │ │ ├── users.list.flow.yaml │ │ └── users.detail.flow.yaml │ ├── data/ # 假資料(E2E 與 Mock 使用) │ │ ├── users.list.v1.json │ │ ├── users.detail.u_1.v1.json │ │ └── seed/ │ │ └── users.seed.json │ ├── plugins/ # JS 外掛(goja) │ │ ├── search.js │ │ └── helpers.js │ └── validators/ # 前後端共用 schema 定義(可選) │ ├── zod.ts │ └── ajv.ts │ ├── gateway/ # Artifact Gateway(Golang) │ ├── cmd/ │ │ └── artifact-gateway/ │ │ └── main.go # 啟動 HTTP、讀取 REPO_BASE、註冊路由 /mock /repo │ ├── internal/ │ │ ├── gateway/ │ │ │ ├── server.go # 路由、middleware(RBAC、logging、recover) │ │ │ └── match.go # 依 repo/api/index.json 匹配端點 │ │ └── flows/ │ │ ├── engine.go # Context、Step、OperatorRegistry、RespondStep │ │ ├── dataset.go # DataSource 介面、MemoryDataSource │ │ ├── ops.go # 運算子:filterAndPaginate、findById、checkEmailUnique、assignId、js 代理 │ │ ├── plugins.go # goja VM、外掛搜尋路徑、函式執行 │ │ ├── validator.go # request/response schema 驗證(可接 gojsonschema) │ │ ├── rbac.go # x-role / JWT claims 驗證(開發期) │ │ └── ops_test.go # 單元測試(涵蓋四個運算子與 JS 外掛) │ ├── testdata/ │ │ ├── repo/ # 測試專用 repo 拷貝 │ │ │ ├── api/index.json │ │ │ ├── data/users.list.v1.json │ │ │ └── plugins/search.js │ │ └── requests/ │ │ └── users.create.json │ ├── Dockerfile │ └── go.mod │ ├── backend/ # 真實後端服務(Golang) │ ├── cmd/ │ │ └── api/ │ │ └── main.go # 真實 API 入口(生產使用) │ ├── pkg/ │ │ ├── gen/ # oapi-codegen 生成 │ │ │ └── openapi.gen.go │ │ ├── handlers/ │ │ │ ├── users.go │ │ │ └── health.go │ │ ├── middlewares/ │ │ │ ├── logger.go │ │ │ └── auth.go │ │ ├── models/ │ │ │ └── user.go │ │ ├── storage/ │ │ │ └── memory.go │ │ └── server/ │ │ └── router.go │ ├── api/ │ │ └── openapi.yaml # 從 /api 同步或軟鏈接,供 oapi-codegen │ ├── Dockerfile │ ├── go.mod │ └── go.sum │ ├── frontend/ # Angular 前端 │ ├── src/ │ │ ├── app/ │ │ │ ├── core/ │ │ │ │ ├── api/ │ │ │ │ │ ├── types.gen.ts # openapi-typescript 生成 │ │ │ │ │ └── zod.gen.ts # zod schema(由腳本生成或手寫) │ │ │ │ ├── artifact/ │ │ │ │ │ ├── artifact.service.ts # 以 /mock 或 /repo 讀取資料 │ │ │ │ │ └── artifact.spec.ts # 與 repo/data 致性測試 │ │ │ │ ├── datasource/ │ │ │ │ │ ├── datasource.service.ts # 資料來源狀態管理(Mock/Repo/API) │ │ │ │ │ ├── datasource.interceptor.ts# 附加 header 與監測請求 │ │ │ │ │ └── datasource.model.ts │ │ │ │ ├── config/config.service.ts │ │ │ │ └── utils/http-error.util.ts │ │ │ ├── features/ │ │ │ │ └── users/ │ │ │ │ ├── users-list/ │ │ │ │ │ ├── users-list.component.ts │ │ │ │ │ └── users-list.component.html │ │ │ │ └── users-detail/ │ │ │ │ ├── users-detail.component.ts │ │ │ │ └── users-detail.component.html │ │ │ ├── shared/ │ │ │ │ ├── components/ │ │ │ │ └── pipes/ │ │ │ └── devtools/ │ │ │ ├── datasource-switcher/ │ │ │ │ ├── datasource-switcher.component.ts │ │ │ │ ├── datasource-switcher.component.html │ │ │ │ └── datasource-switcher.component.scss │ │ │ └── request-log-panel/ │ │ │ ├── request-log-panel.component.ts │ │ │ └── request-log-panel.component.html │ │ ├── environments/ │ │ │ ├── environment.ts # dev:預設 /mock │ │ │ └── environment.prod.ts # prod:預設 /api │ │ ├── main.ts │ │ └── styles.scss │ ├── cypress/ # e2e 測試 │ │ ├── e2e/ │ │ │ ├── datasource-switch.cy.ts │ │ │ └── users-flow.cy.ts │ │ └── support/ │ │ └── e2e.ts │ ├── Dockerfile │ ├── angular.json │ ├── package.json │ └── tsconfig.json │ ├── config/ │ ├── cloudbuild.yaml # GCP Cloud Build(或 GitHub Actions 對等) │ └── k8s/ │ ├── namespace.yaml │ ├── gateway.deployment.yaml │ ├── gateway.service.yaml │ ├── backend.deployment.yaml │ ├── backend.service.yaml │ └── ingress.yaml │ ├── artifacts/ │ ├── template.yaml # Artifact 模板與鏡像標籤參數 │ ├── docker-compose.dev.yaml # 本地整包開發(含 repo 暴露與 /mock 代理) │ └── helm/ │ └── my-app/ │ ├── Chart.yaml │ ├── values.yaml │ └── templates/ │ ├── deployment.yaml │ ├── service.yaml │ └── ingress.yaml │ └── .github/ └── workflows/ └── ci.yml # GitHub Actions(或與 Cloud Build 並存)
最新发布
10-28
├── api # API 接口定义优化‌ │ ├── v1 # 版本1接口定义 │ │ ├── router.go # 版本1路由定义 ├── config # 配置中心 │ ├── env # 🌟新增多环境配置‌: │ │ ├── dev.yaml # 开发环境配置 │ │ └── prod.yaml # 生产环境配置 │ ├── config.go # Viper 配置加载器 │ ├── hot_reload.go # 🌟新增配置热更新 │ └── settings.go # 配置结构体定义 ├── controllers # HTTP 控制器优化 │ ├── auth.go # 认证相关接口 │ ├── base.go # 🌟新增基础控制器 │ └── user.go # 用户管理接口 ├── db # 数据模块增强 │ ├── healthcheck.go # 🌟新增健康检查 │ ├── mysql.go # GORM 主连接池 │ ├── redis.go # Redis 集群连接 │ ├── mongo.go # 官方驱动封装 │ └── transaction.go # 🌟新增事务管理 ├── models # 数据模型优化‌ │ ├── user.go # 用户模型(GORM) │ ├── order.go # 订单模型 │ └── base_model.go # 🌟新增模型基类 ├── middleware # 中间件增强 │ ├── auth.go # JWT 认证中间件 │ ├── cors.go # CORS 中间件 │ ├── timeout.go # 🌟新增超时控制 │ ├── logger.go # Zap 日志中间件 │ └── limiter.go # 🌟新增接口请求限流 ├── services # 服务层重构‌ │ ├── auth.go # 认证服务接口+实现 │ ├── user.go # 用户服务接口+实现 │ └── service.go # 🌟新增服务基类 ├── utils # 工具类安全改造‌ │ ├── auth.go # 增强 JWT 工具 │ ├── helper.go # 通用工具函数 │ └── crypto.go # 🌟新增 AES 加密 ├── docs # 🌟新增 API 文档‌: │ ├── docs.go # Swagger 文档生成器 │ ├── swagger.json # 静态Swagger文档,用于离线查阅或调试工具导入 │ └── swagger.yaml # 动态Swagger文档,用于在线API文档展示 ├── logs # 日志文件存放目录 │ └── 2025-01-01.log # 日志文件 ├── main.go # 启动入口优化‌ └── README.md # 项目文档增强‌ 该项目如何实现: ‌流量治理; 数据扩展性; 运维可视化; 安全闭环
03-11
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值