前言
在当今微服务和分布式系统盛行的背景下,安全、高效的用户身份验证机制显得尤为重要。为了有效管理用户的访问权限并验证用户身份,我们经常会采用各种身份验证方案。而 JSON
Web
Tokens
(JWT
)便是其中一种流行的技术,因其简洁、灵活且易于跨语言实现的特性,被广泛应用于系统的身份验证和信息交换。
本文旨在介绍如何在 Go
语言中使用 JWT
。内容将涵盖 JWT
的简单介绍、安装 Go
JWT
模块、创建 JWT
对象、生成 JWT
字符串以及解析 JWT
字符串等方面。
准备好了吗?准备一杯你最喜欢的咖啡或茶,随着本文一探究竟吧。
JWT
JSON Web Token
(JWT
)是一种开放标准(RFC 7519),用于在网络应用环境间安全地传递声明(claims
)。JWT
是一种紧凑且自包含的方式,用于作为 JSON
对象在各方之间安全地传输信息。由于其信息是经过数字签名的,所以可以确保发送的数据在传输过程中未被篡改。
JWT
由三个部分组成,它们之间用 .
分隔,格式如下:Header.Payload.Signature
→ eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJQcm9ncmFtbWVyIiwiaXNzIjoi56iL5bqP5ZGY6ZmI5piO5YuHIiwic3ViIjoiY2hlbm1pbmd5b25nLmNuIn0.uRnH-rUb7lsZtQ11o8wXjIOJnIMBxszkvU1gY6hCGjo
,下面对每个部分的进行简要介绍:
1.Header(头部)
:Hedaer
部分用于描述该 JWT
的基本信息,比如其类型(通常是 JWT
)以及所使用的签名算法(如 HMAC SHA256
或 RSA
)。
2.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
的双方共同商定的声明,区别于 注册声明 和 公共声明。
3.Signature(签名)
:为了防止数据篡改,将头部和负载的信息进行一定算法处理,加上一个密钥,最后生成签名。如果使用的是 HMAC SHA256
算法,那么签名就是将编码后的头部、编码后的负载拼接起来,通过密钥进行HMAC SHA256
运算后的结果。
实战
安装
通过以下命令在 Go
程序里安装 Go JWT
依赖:
go get -u github.com/golang-jwt/jwt/v5
创建 Token(JWT) 对象
生成 JWT
字符串首先需要创建 Token
对象(代表着一个 JWT
)。因此我们需要先了解如何创建 Token
对象。
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
的时候对其进行进一步的配置。
使用示例
代码语言:go复制// https://github.com/chenmingyong0423/blog/blob/master/tutorial-code/go/jwt/create-token/new_with_claims.go
package main
import (
"fmt"
"github.com/golang-jwt/jwt/v5"
)
func main() {
mapClaims := jwt.MapClaims{
"iss": "程序员陈明勇",
"sub": "chenmingyong.cn",
"aud": "Programmer",
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, mapClaims)
fmt.Println(token != nil) // true
}
这段代码首先构建了包含发行者(iss
)、主题(sub
)和观众(aud
)信息的 MapClaims
类型声明。
然后,通过调用 jwt.NewWithClaims
函数,并将 jwt.SigningMethodHS256
作为签名方法和之前构建的 mapClaims
作为参数传递,来创建了一个新的 Token
实例。
New 函数
jwt.New
函数用于创建一个 Token
对象,该函数允许指定一个签名方法和可变参数 TokenOption
。下面是该函数的源码:
func New(method SigningMethod, opts ...TokenOption) *Token {
return NewWithClaims(method, MapClaims{}, opts...)
}
通过源码我们可以发现,该函数内部的实现通过调用 NewWithClaims
函数,并默认传入一个空的 MapClaims
对象,从而生成一个 Token
对象。
使用示例
代码语言:go复制// https://github.com/chenmingyong0423/blog/blob/master/tutorial-code/go/jwt/create-token/new.go
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
字符串,另一个是在签名过程中遇到的任何错误。
使用示例
代码语言:go复制// 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": "程序员陈明勇",
"sub": "chenmingyong.cn",
"aud": "Programmer",
})
if err != nil {
panic(err)
}
fmt.Println(jwtStr)
}
这段代码首先声明并初始化一个长度为 32 字节的 byte
切片,然后使用 crypto/rand
库的 Read
函数填充切片(即密钥),确保生成的密钥具有高强度的随机性和不可预测性。
然后,调用 GenerateJwt
函数,传入 jwtKey
、jwt.SigningMethodHS256
签名方法和包含特定声明的 MapClaims
对象,以创建 JWT
字符串。
在 GenerateJwt
函数内部,它利用 token.SignedString
方法和提供的 key
生成并返回签名的 JWT
字符串。
解析 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
声明为必需的参数,否则解析失败。
使用示例
代码语言:go复制// 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": "程序员陈明勇",
"sub": "chenmingyong.cn",
"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)
}
这段代码的重点是自定义的 ParseJwt
函数,它负责解析 JWT
字符串,并根据验证结果返回 Claims
数据和一个可能的存在的错误。ParseJwt
函数内部利用 jwt.Parse
解析 JWT
字符串。解析后,函数检查得到的 token
对象的 Valid
属性以确认 Claims
是否有效。有效性检查包括但不限于验证签名、检查 token
是否过期。如果 token
通过所有验证,函数返回 Claims
数据;如果验证失败(如签名不匹配或 token
已过期),则返回错误。
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
函数中的相同,用来定制解析器的行为.
使用示例
代码语言:go复制// 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": "程序员陈明勇",
"sub": "chenmingyong.cn",
"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) // map[aud:Programmer iss:程序员陈明勇 sub:chenmingyong.cn]
}
这段代码中的 ParseJwtWithClaims
函数与之前示例中的 ParseJwt
函数功能类似,都是负责解析 JWT
字符串,并根据验证结果返回 Claims
数据和一个可能的存在的错误。不同之处在于,ParseJwtWithClaims
函数内部使用了 jwt.ParseWithClaims
函数来解析 JWT
字符串,这额外要求我们提供一个 Claims
实例来接收解析后的 claims
数据。在此示例中,通过 jwt.MapClaims
提供了这一实例。
小结
本文首先对 JWT
进行了概述,随后深入讲解了在 Go
语言下使用 JWT
的全过程。内容包括安装 Go
的 JWT
模块、创建 JWT
对象、生成 JWT
字符串以及解析 JWT
字符串的详细指南。