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的優勢與劣勢
優點:
- 多語言支援
- 通用性好,不存在跨域問題
- 資料簽名相對安全。
- 不需要服務端集中維護token資訊,便於擴充套件。
缺點:
1、使用者無法主動登出,只要token在有效期內就有效。這裡可以考慮redis設定同token有效期一直的黑名單解決此問題。
2、token過了有效期,無法續簽問題。可以考慮透過判斷舊的token什麼時候到期,過期的時候重新整理token續簽介面產生新token代替舊token
JWT的構成
Header
Header是頭部
Jwt的頭部承載兩部分資訊:
宣告型別,這裡是jwt
宣告加密的演算法 通常直接使用 HMAC SHA256
Playload(載荷又稱為Claim)
playload可以填充兩種型別資料
簡單來說就是 比如使用者名稱、過期時間等,
- 標準中註冊的宣告
iss: 簽發者
sub: 面向的使用者
aud: 接收方
exp: 過期時間
nbf: 生效時間
iat: 簽發時間
jti: 唯一身份標識
- 自定義宣告
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丟到上面網站中解析一下
可以發現 有三部分被解析出來了
- Header 告訴我們用的是什麼演算法,型別是什麼
- PayLoad 我們自定義的一些資料
- Signature 之後伺服器解析做的簽名驗證
程式碼解析token
- 宣告一個空的資料宣告
- 呼叫 jwt.ParseWithClaims 方法
- 傳入token 資料宣告介面,
- 判斷Token是否有效
- 返回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)
}
這裡需要注意
使用者請求時帶上token,伺服器解析token後可以獲得其中的使用者資訊,如果token有任何改動,都無法透過驗證.