JWT 概念 #
JSON Web Token(JWT)是一种开放标准(RFC 7519),用于在网络应用环境间安全地传递声明(claims)。JWT 是一种紧凑且自包含的方式,用于作为 JSON 对象在各方之间安全地传输信息。由于其信息是经过数字签名的,所以可以确保发送的数据在传输过程中未被篡改。
JWT 由三个部分组成,它们之间用 . 分隔,格式如下:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJQcm9ncmFtbWVyIiwiaXNzIjoi56iL5bqP5ZGY6ZmI5piO5YuHIiwic3ViIjoiY2hlbm1pbmd5b25nLmNuIn0.uRnH-rUb7lsZtQ11o8wXjIOJnIMBxszkvU1gY6hCGjo
Header(头部):Hedaer部分用于描述该JWT的基本信息,比如其类型(通常是JWT)以及所使用的签名算法(如HMAC SHA256或RSA)。Payload(负载):Payload部分包含所传递的声明。声明是关于实体(通常是用户)和其他数据的语句。声明可以分为三种类型:注册声明、公共声明 和 私有声明。
注册声明:这些声明是预定义的,非必须使用的但被推荐使用。官方标准定义的注册声明有 7 个:
| Claim(声明) | 含义 |
|---|---|
iss(Issuer) |
发行者,标识 JWT 的发行者。 |
sub(Subject) |
主题,标识 JWT 的主题,通常指用户的唯一标识 |
aud(Audience) |
观众,标识 JWT的接收者 |
exp(Expiration Time) |
过期时间。标识 JWT 的过期时间,这个时间必须是将来的 |
nbf(Not Before) |
不可用时间。在此时间之前,JWT 不应被接受处理 |
iat(Issued At) |
发行时间,标识 JWT 的发行时间 |
jti(JWT ID) |
JWT 的唯一标识符,用于防止 JWT 被重放(即重复使用) |
公共声明:可以由使用 JWT 的人自定义,但为了避免冲突,任何新定义的声明都应已在IANA JSON Web Token Registry中注册或者是一个 公共名称,其中包含了碰撞防抗性名称(Collision-Resistant Name)。
私有声明:发行和使用 JWT 的双方共同商定的声明,区别于 注册声明 和 公共声明。
Signature(签名):为了防止数据篡改,将头部和负载的信息进行一定算法处理,加上一个密钥,最后生成签名。如果使用的是HMAC SHA256算法,那么签名就是将编码后的头部、编码后的负载拼接起来,通过密钥进行HMAC SHA256运算后的结果。
golang-jwt #
go get -u github.com/golang-jwt/jwt/v5
创建 Token(JWT) 对象 #
生成 JWT 字符串首先需要创建 Token 对象(代表着一个 JWT)。
jwt 库主要通过两个函数来创建 Token 对象:NewWithClaims 和 New。
NewWithClaims 函数 #
jwt.NewWithClaims 函数用于创建一个 Token 对象,该函数允许指定一个签名方法和一组声明claims以及可变参数 TokenOption。
NewWithClaims(method SigningMethod, claims Claims, opts ...TokenOption) *Token
method:这是一个SigningMethod接口参数,用于指定JWT的签名算法。常用的签名算法有SigningMethodHS256、SigningMethodRS256等。这些算法分别代表不同的签名技术,如HMAC、RSA。claims:这是一个Claims接口参数,它表示JWT的声明。在jwt库中,预定义了一些结构体来实现这个接口,例如RegisteredClaims和MapClaims等,通过指定Claims的实现作为参数,我们可以为JWT添加声明信息,例如发行人(iss)、主题(sub)等。opts:这是一个可变参数,允许传递零个或多个TokenOption类型参数。TokenOption是一个函数,它接收一个*Token,这样就可以在创建Token的时候对其进行进一步的配置。
package main
import (
"fmt"
"github.com/golang-jwt/jwt/v5"
)
func main() {
mapClaims := jwt.MapClaims{
"iss": "GradyYoung",
"sub": "ygang.top",
"aud": "Programmer",
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, mapClaims)
fmt.Println(token != nil) // true
}
New 函数 #
jwt.New 函数用于创建一个 Token 对象,该函数允许指定一个签名方法和可变参数 TokenOption。
func New(method SigningMethod, opts ...TokenOption) *Token {
return NewWithClaims(method, MapClaims{}, opts...)
}
通过源码我们可以发现,该函数内部的实现通过调用 NewWithClaims 函数,并默认传入一个空的 MapClaims 对象,从而生成一个 Token 对象。
package main
import (
"fmt"
"github.com/golang-jwt/jwt/v5"
)
func main() {
token := jwt.New(jwt.SigningMethodHS256)
fmt.Println(token != nil) // true
}
生成 JWT 字符串 #
通过使用 jwt.Token 对象的 SignedString 方法,我们能够对 JWT 对象进行序列化和签名处理,以生成最终的 token 字符串。该方法的签名如下:
func (t *Token) SignedString(key interface{}) (string, error)
key:该参数是用于签名token的密钥。密钥的类型取决于使用的签名算法。- 如果使用
HMAC算法(如HS256、HS384等),key应该是一个对称密钥(通常是[]byte类型的密钥)。 - 如果使用
RSA或ECDSA签名算法(如RS256、ES256),key应该是一个私钥*rsa.PrivateKey或*ecdsa.PrivateKey。
- 如果使用
- 方法返回两个值:一个是成功签名后的
JWT字符串,另一个是在签名过程中遇到的任何错误。
// https://github.com/chenmingyong0423/blog/blob/master/tutorial-code/go/jwt/generage-token/generate_token.go
package main
import (
"crypto/rand"
"fmt"
"github.com/golang-jwt/jwt/v5"
)
func GenerateJwt(key any, method jwt.SigningMethod, claims jwt.Claims) (string, error) {
token := jwt.NewWithClaims(method, claims)
return token.SignedString(key)
}
func main() {
jwtKey := make([]byte, 32) // 生成32字节(256位)的密钥
if _, err := rand.Read(jwtKey); err != nil {
panic(err)
}
jwtStr, err := GenerateJwt(jwtKey, jwt.SigningMethodHS256, jwt.MapClaims{
"iss": "GradyYoung",
"sub": "ygang.top",
"aud": "Programmer",
})
if err != nil {
panic(err)
}
fmt.Println(jwtStr)
}
解析 JWT 字符串 #
jwt 库主要通过两个函数来解析 jwt 字符串:Parse 和 ParseWithClaims。
Parse 函数 #
Parse 函数用于解析 JWT 字符串,函数签名如下:
func Parse(tokenString string, keyFunc Keyfunc, options ...ParserOption) (*Token, error)
tokenString:要解析的JWT字符串。keyFunc:这是一个回调函数,返回用于验证JWT签名的密钥。该函数签名为func(*Token) (interface{}, error)。这种设计,有利于我们根据token对象的信息返回正确的密钥。例如我们可能有一个keyMap对象,类型为map,该对象用于保存多个key的映射,通过Token对象的信息,拿到某个标识,就能通过keyMap获取到正确的密钥。options:这是一个可变参数。允许传递零个或多个ParserOption类型参数。这些选项可以用来定制解析器的行为,如设置exp声明为必需的参数,否则解析失败。
// https://github.com/chenmingyong0423/blog/blob/master/tutorial-code/go/jwt/parse-token/parse.go
package main
import (
"crypto/rand"
"errors"
"fmt"
"github.com/golang-jwt/jwt/v5"
"time"
)
func ParseJwt(key any, 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
}
// 校验 Claims 对象是否有效,基于 exp(过期时间),nbf(不早于),iat(签发时间)等进行判断(如果有这些声明的话)。
if !token.Valid {
return nil, errors.New("invalid token")
}
return token.Claims, nil
}
func main() {
jwtKey := make([]byte, 32) // 生成32字节(256位)的密钥
if _, err := rand.Read(jwtKey); err != nil {
panic(err)
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"iss": "GradyYoung",
"sub": "ygang.top",
"aud": "Programmer",
"exp": time.Now().Add(time.Second * 10).UnixMilli(),
})
jwtStr, err := token.SignedString(jwtKey)
if err != nil {
panic(err)
}
// 解析 jwt
claims, err := ParseJwt(jwtKey, jwtStr, jwt.WithExpirationRequired())
if err != nil {
panic(err)
}
fmt.Println(claims)
}
ParseWithClaims 函数 #
ParseWithClaims 函数类似 Parse,函数签名如下:
func ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc, options ...ParserOption) (*Token, error)
tokenString:要解析的JWT字符串。claims:这是一个Claims接口参数,用于接收解析JWT后的claims数据。keyFunc:与Parse函数中的相同,用于提供验证签名所需的密钥。options:与Parse函数中的相同,用来定制解析器的行为。
// https://github.com/chenmingyong0423/blog/blob/master/tutorial-code/go/jwt/parse-token/parse_with_claims.go
package main
import (
"crypto/rand"
"errors"
"fmt"
"github.com/golang-jwt/jwt/v5"
)
func ParseJwtWithClaims(key any, jwtStr string, options ...jwt.ParserOption) (jwt.Claims, error) {
mc := jwt.MapClaims{}
token, err := jwt.ParseWithClaims(jwtStr, mc, func(token *jwt.Token) (interface{}, error) {
return key, nil
}, options...)
if err != nil {
return nil, err
}
// 校验 Claims 对象是否有效,基于 exp(过期时间),nbf(不早于),iat(签发时间)等进行判断(如果有这些声明的话)。
if !token.Valid {
return nil, errors.New("invalid token")
}
return token.Claims, nil
}
func main() {
jwtKey := make([]byte, 32) // 生成32字节(256位)的密钥
if _, err := rand.Read(jwtKey); err != nil {
panic(err)
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"iss": "GradyYoung",
"sub": "ygang.top",
"aud": "Programmer",
})
jwtStr, err := token.SignedString(jwtKey)
if err != nil {
panic(err)
}
// 解析 jwt
claims, err := ParseJwtWithClaims(jwtKey, jwtStr)
if err != nil {
panic(err)
}
fmt.Println(claims)
}
JWT 工具 #
package utils
import (
"errors"
"github.com/golang-jwt/jwt/v5"
"time"
)
// Claims 自定义 JWT 声明
type Claims struct {
UserId uint `json:"userId"`
jwt.RegisteredClaims
}
// GenerateToken 生成 JWT 令牌
func GenerateToken(userId uint, secret string, expirySecond int) (string, error) {
// 设置过期时间
now := time.Now()
expirationTime := now.Add(time.Duration(expirySecond) * time.Second)
// 创建声明
claims := &Claims{
UserId: userId,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationTime),
IssuedAt: jwt.NewNumericDate(now),
},
}
// 创建令牌
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// 签名令牌
signedString, err := token.SignedString([]byte(secret))
if err != nil {
return "", err
}
return signedString, nil
}
// ValidateToken 验证令牌
func ValidateToken(tokenString, secret string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(secret), nil
})
if err != nil {
return nil, err
}
if !token.Valid {
return nil, errors.New("令牌无效!")
}
claims, ok := token.Claims.(*Claims)
if !ok {
return nil, errors.New("无法获取令牌声明!")
}
return claims, nil
}