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。
实际使用例子
package jwts
import (
"errors"
"time"
"github.com/golang-jwt/jwt/v4"
)
type JwtPayLoad struct {
UserID uint `json:"user_id"`
UserName string `json:"username"` //用户名
Role int `json:"role"` //权限 1. 普通用户 2.管理员
}
type CustomClaims struct {
JwtPayLoad
jwt.RegisteredClaims
}
func GenerateToken(payload JwtPayLoad, accessSecret string, expires int) (string, error) {
claim := CustomClaims{
JwtPayLoad: payload,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * time.Duration(expires))),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claim)
return token.SignedString([]byte(accessSecret))
}
func ParseToken(tokenStr string, accessSecret string) (*CustomClaims, error) {
token, err := jwt.ParseWithClaims(tokenStr, &CustomClaims{}, func(token *jwt.Token) (any, error) {
return []byte(accessSecret), nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
return claims, nil
}
return nil, errors.New("invalid token")
}
package jwts
import "testing"
func TestJwt(t *testing.T) {
str, err := GenerateToken(JwtPayLoad{
UserID: 1,
UserName: "meowrain",
Role: 1,
}, "12345", 8)
if err != nil {
t.Error(err)
}
t.Logf("Jwt string: %s", str)
//Jwt string: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6Im1lb3dyYWluIiwicm9sZSI6MSwiZXhwIjoxNzI2Mzk3NDA3fQ.s4J0Z-oR5j9GwaPfOxldu8efVqJeEMDdyXrUYw4nXfE
}
func TestJwtParse(t *testing.T) {
str := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6Im1lb3dyYWluIiwicm9sZSI6MSwiZXhwIjoxNzI2Mzk3MzA1fQ.ApAY7OD2Lu2FihmpyuBO5y72ufsdGCkpotXbyasBKmo"
customClaims, err := ParseToken(str, "12345")
if err != nil {
t.Error(err)
}
t.Logf("Parse Data: %v", customClaims)
/*
Running tool: /usr/sbin/go test -timeout 30s -run ^TestJwtParse$ im-backend/utils/jwts
=== RUN TestJwtParse
/home/meowrain/projects/im_backend/utils/jwts/jwt_test.go:23: Parse Data: &{{1 meowrain 1} { [] 2024-09-15 18:48:25 +0800 CST <nil> <nil> }}
--- PASS: TestJwtParse (0.00s)
PASS
ok im-backend/utils/jwts 0.003s
*/
}