Golang 一日一庫之jwt-go

2023-04-11 15:00:34

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

github地址:https://github.com/dgrijalva/jwt-go

何為 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

https://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有任何改動,都無法通過驗證.