参考 https://juejin.cn/post/7342766597243207715?searchId=20240903160011693AB9B562C29F25EA11
http://ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html
JWT 介紹
JWT 即 JSON web Token ,用於在網絡應用環境中安全地傳遞聲明claims
JWT 是一种紧凑且自包含的方式,用于作为 JSON 对象在各方之间安全地传输信息。由于其信息是经过数字签名的,所以可以确保发送的数据在传输过程中未被篡改。
JWT 組成結構
JWT 由三个部分组成,它们之间用 . 分隔,格式如下:Header.Payload.Signature、
-
Header: Header部分用於描述該JWT的基本信息,比如其類型和所使用的算法
-
Payload(負載): Payload部分包含所傳遞的聲明,聲明是關於實體和其他數據的語句。聲明能分爲三種類型:
> 注冊聲明
> 公共聲明
> 私有聲明
> -
Signature(簽名): 为了防止数据篡改,将头部和负载的信息进行一定算法处理,加上一个密钥,最后生成签名。如果使用的是 HMAC SHA256 算法,那么签名就是将编码后的头部、编码后的负载拼接起来,通过密钥进行HMAC SHA256 运算后的结果。
Signature 部分是对前两部分的签名,防止数据篡改。
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
HMACSHA256(
base64UrlEncode(header) + “.” +
base64UrlEncode(payload),
secret)
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。
實際使用
安裝go-jwt
go get -u github.com/golang-jwt/jwt/v5
创建 Token(JWT) 对象
生成 JWT 字符串首先需要创建 Token 对象(代表着一个 JWT)。因此我们需要先了解如何创建 Token 对象。
jwt 库主要通过两个函数来创建 Token 对象:NewWithClaims 和 New。
jwt.NewWithClaims 函数用于创建一个 Token 对象,该函数允许指定一个签名方法和一组声明claims)以及可变参数 TokenOption。下面是该函数的签名:
我們看源碼,可以發現它接受method->加密算法,claims->負載中的聲明部分,opts是TokenOption,返回一個Token對象的指針,這個函數給Token賦值了Method,Header也就是jwt的頭部,還有Claims負載中的聲明部分。
// Token represents a JWT Token. Different fields will be used depending on
// whether you're creating or parsing/verifying a token.
type Token struct {
Raw string // Raw contains the raw token. Populated when you [Parse] a token
Method SigningMethod // Method is the signing method used or to be used
Header map[string]interface{} // Header is the first segment of the token in decoded form
Claims Claims // Claims is the second segment of the token in decoded form
Signature []byte // Signature is the third segment of the token in decoded form. Populated when you Parse a token
Valid bool // Valid specifies if the token is valid. Populated when you Parse/Verify a token
}
以上為Token對象
我們再研究一下TokenOptions是什麽東西
// TokenOption is a reserved type, which provides some forward compatibility,
// if we ever want to introduce token creation-related options.
type TokenOption func(*Token)
這段註解說明了 TokenOption 是一個保留的類型,其主要目的是提供前向相容性。如果將來在你的系統中需要引入與 token 相關的選項或配置,這個類型可以幫助你更方便地進行擴展或修改。
換句話說,現在即使 TokenOption 可能沒有具體的用途或屬性,它仍然被保留在系統中,以便未來添加新功能時不會影響現有的代碼或結構。這是一種常見的設計模式,用來確保系統在將來需要添加新功能或更改現有功能時,可以保持相對穩定和靈活。
好,現在我們就用這個函數創建一個Token對象吧(這裏其實已經創建好Header.Payload 部分了
MapClaims 是 Go 言語中的一個類型,通常用於處理 JSON Web Token (JWT) 的聲明(claims)。在 jwt-go 庫中,MapClaims 是一個基於地圖(map[string]interface{})的結構,用來存儲和處理 JWT 的聲明。
// MapClaims is a claims type that uses the map[string]interface{} for JSON
// decoding. This is the default claims type if you don't supply one
type MapClaims map[string]interface{}
package main
import (
"fmt"
"github.com/golang-jwt/jwt/v5"
)
func main() {
mapClaims := jwt.MapClaims{
"iss": "meowrain",
"sub": "jwt_learn",
"aud": "public",
}
var token *jwt.Token = jwt.NewWithClaims(jwt.SigningMethodHS256, mapClaims)
fmt.Println(token)
}
我們還能用jwt.New()函數創建Token對象
我們來看一下源碼,可以看到這個函數只接受一個簽名算法,一個TokenOption,然後調用了我們前面説的NewWithClaims函數
,但是在clamis參數上傳遞了一個空的MapClaimsal
// New creates a new [Token] with the specified signing method and an empty map
// of claims. Additional options can be specified, but are currently unused.
func New(method SigningMethod, opts ...TokenOption) *Token {
return NewWithClaims(method, MapClaims{}, opts...)
}
package main
import (
"fmt"
"github.com/golang-jwt/jwt/v5"
)
func main() {
mapClaims := jwt.MapClaims{
"iss": "meowrain",
"sub": "jwt_learn",
"aud": "public",
}
var token *jwt.Token = jwt.NewWithClaims(jwt.SigningMethodHS256, mapClaims)
var token2 *jwt.Token = jwt.New(jwt.SigningMethodHS256)
token2.Claims = mapClaims
fmt.Println(token)
fmt.Println(token2)
}
生成JWT字符串
通过使用 jwt.Token 对象的 SignedString 方法,我们能够对 JWT 对象进行序列化和签名处理,以生成最终的 token 字符串。该方法的签名如下:
// SignedString creates and returns a complete, signed JWT. The token is signed
// using the SigningMethod specified in the token. Please refer to
// https://golang-jwt.github.io/jwt/usage/signing_methods/#signing-methods-and-key-types
// for an overview of the different signing methods and their respective key
// types.
func (t *Token) SignedString(key interface{}) (string, error) {
sstr, err := t.SigningString()
if err != nil {
return "", err
}
sig, err := t.Method.Sign(sstr, key)
if err != nil {
return "", err
}
return sstr + "." + t.EncodeSegment(sig), nil
}
这个函数接收一个key,类型为byte数组,我们传递进去
package main
import (
"fmt"
"github.com/golang-jwt/jwt/v5"
)
func main() {
mapClaims := jwt.MapClaims{
"iss": "meowrain",
"sub": "jwt_learn",
"aud": "public",
}
var token *jwt.Token = jwt.NewWithClaims(jwt.SigningMethodHS256, mapClaims)
jwt_str, err := token.SignedString([]byte("chat"))
if err != nil {
panic(err)
}
fmt.Println(token)
fmt.Println(jwt_str)
}
JWT解析
jwt 库主要通过两个函数来解析 jwt 字符串:Parse 和 ParseWithClaims。
// Parse parses, validates, verifies the signature and returns the parsed token.
// keyFunc will receive the parsed token and should return the cryptographic key
// for verifying the signature. The caller is strongly encouraged to set the
// WithValidMethods option to validate the 'alg' claim in the token matches the
// expected algorithm. For more details about the importance of validating the
// 'alg' claim, see
// https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/
func Parse(tokenString string, keyFunc Keyfunc, options ...ParserOption) (*Token, error) {
return NewParser(options...).Parse(tokenString, keyFunc)
}
我們來看看keyFunc這個結構體組成
// Keyfunc will be used by the Parse methods as a callback function to supply
// the key for verification. The function receives the parsed, but unverified
// Token. This allows you to use properties in the Header of the token (such as
// `kid`) to identify which key to use.
//
// The returned interface{} may be a single key or a VerificationKeySet containing
// multiple keys.
type Keyfunc func(*Token) (interface{}, error)
可以看到這個函數是一個解析的回調函數,提供key來進行驗證
通用代碼
package main
import (
"errors"
"fmt"
"github.com/golang-jwt/jwt/v5"
)
func GenerateJwt(claims *jwt.MapClaims, key string) (string, error) {
var token *jwt.Token = jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
jwt_str, err := token.SignedString([]byte(key))
if err != nil {
return "", err
}
return jwt_str, nil
}
func ParseJwt(key []byte, jwtStr string, options ...jwt.ParserOption) (jwt.Claims, error) {
token, err := jwt.Parse(jwtStr, func(token *jwt.Token) (interface{}, error) {
return key, nil
}, options...)
if err != nil {
return nil, err
}
if !token.Valid {
return nil, errors.New("invalid token")
}
return token.Claims, nil
}
func main() {
mapClaims := jwt.MapClaims{
"iss": "meowrain",
"sub": "jwt_learn",
"aud": "public",
}
var token *jwt.Token = jwt.NewWithClaims(jwt.SigningMethodHS256, mapClaims)
jwt_str, err := token.SignedString([]byte("chat"))
if err != nil {
panic(err)
}
fmt.Println(token)
fmt.Println(jwt_str)
claims, err := ParseJwt([]byte("chat"), jwt_str)
if err != nil {
panic(err)
}
fmt.Println(claims)
}
jwt.RegisterClaims和jwt.MapClaims
在 Go 的 JWT 库中,jwt.RegisteredClaims
和 jwt.MapClaims
都是用于处理 JWT(JSON Web Token)的声明(claims)的类型,但它们有一些区别:
-
jwt.RegisteredClaims
:- 它是一个结构体,包含了一些 JWT 标准定义的注册声明(registered claims),如:
Issuer
(iss
): Token 的发行者。Subject
(sub
): Token 的主题。Audience
(aud
): Token 的接收者。Expiration
(exp
): Token 的过期时间。NotBefore
(nbf
): Token 在此时间之前无效。IssuedAt
(iat
): Token 的签发时间。ID
(jti
): Token 的唯一标识符。
- 使用
RegisteredClaims
更方便处理标准声明,因为它提供了明确的字段和类型。
示例:
claims := jwt.RegisteredClaims{ Issuer: "example.com", Subject: "user_id", Audience: []string{"example.com"}, ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)), }
- 它是一个结构体,包含了一些 JWT 标准定义的注册声明(registered claims),如:
-
jwt.MapClaims
:- 它是一个
map[string]interface{}
,可以存储任意键值对。你可以使用它来处理自定义声明或混合标准声明和自定义声明。 - 灵活性更高,但在处理标准声明时需要自己解析这些字段。
示例:
claims := jwt.MapClaims{ "iss": "example.com", "sub": "user_id", "exp": time.Now().Add(time.Hour).Unix(), "customClaim": "custom_value", }
- 它是一个
总结:
RegisteredClaims
适合处理标准 JWT 声明,提供了类型安全和易用性。MapClaims
更灵活,适合处理自定义声明,但需要手动处理标准声明的键值。
你可以根据需求选择其中一种类型来处理 JWT。