jwt-go

Status
Done
Assign
Date
Apr 11, 2023
 

何为 jwt token?

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

jwt的优势与劣势

优点:
  1. 多语言支持
  1. 通用性好,不存在跨域问题
  1. 数据签名相对安全。
  1. 不需要服务端集中维护token信息,便于扩展。
缺点: 1、用户无法主动登出,只要token在有效期内就有效。这里可以考虑redis设置同token有效期一直的黑名单解决此问题。
2、token过了有效期,无法续签问题。可以考虑通过判断旧的token什么时候到期,过期的时候刷新token续签接口产生新token代替旧token

JWT的构成

Header

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接口 还需要定义几个方法
notion image
如上图所示 然后我们使用
使用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强转
notion image
像这个样子。
然后我们去执行 传入一个ID 和一个name
token, _ := utils.GenerateToken(1, "张三") fmt.Println(token)
notion image
得到如下值
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJJRCI6MSwiTmFtZSI6IuW8oOS4iSIsIlJlZ2lzdGVyZWRDbGFpbXMiOnsic3ViIjoiVG9rZW4iLCJleHAiOjE2ODExODI2MDYsImlhdCI6MTY4MTE4MjYwNn19.AmOf60S2xby6GmlGgNo4Q5b01cRoAqXWhGorzxbJ2-Q

解析Token

在写代码之前,我们把上面的token丢到上面网站中解析一下
notion image
可以发现 有三部分被解析出来了
  1. Header 告诉我们用的是什么算法,类型是什么
  1. PayLoad 我们自定义的一些数据
  1. Signature 之后服务器解析做的签名验证
代码解析token
  1. 声明一个空的数据声明
  1. 调用 jwt.ParseWithClaims 方法
  1. 传入token 数据声明接口,
  1. 判断Token是否有效
  1. 返回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 }
返回成功如下图所示
notion image
由于我们主动抛了个错,那我们如果手动传入错的token 看他是否会抛出错误提示呢?
jwtCustomClaim, err := utils.ParseToken(token + "12312323123")
结果:
notion image
答案是会。

完整代码

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 }

制定解析规则

notion image
在自己写的这个函数中 我们点进源码看返回值
notion image
解析方法使用此回调函数提供用于验证的键。函数接收已解析但未验证的令牌。 这允许您使用令牌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有任何改动,都无法通过验证.