代码地址:https://gitee.com/lymgoforIT/golang-trick/tree/master/26-jwt
一、JWT是什么
JWT(JSON Web Token)
是一种基于 JSON
的开放标准,用于在网络应用间传递声明。JWT
可安全地将用户身份验证和授权数据作为 JSON
对象在各个应用程序之间传递。目前JWT 是一种非常流行的身份验证和授权机制,被广泛应用于各种互联网应用中。
JWT
主要由三部分组成:Header,Payload 和Signature
。
-
Header
包含了两部分信息:令牌的类型(即JWT
)和所使用的算法,通常采用的算法是HMAC SHA256
或RSA
。 -
Payload
(载荷)包含了要传递的信息,可以包括用户ID
、用户角色、过期时间等信息。payload中一般会包含jwt.StandardClaims
,该结构体字段有
iss (issuer)(发行人)
sub (subject)(主题)
aud (audienct) (受众)
exp (expiration time)(到期时间)
nbf (not before)(不早于 生效时间)
iat (issued at)(发布于)
jti (jwt id)(JWT ID)
Signature
(签名)是使用私钥对Header
和Payload
进行签名生成的,用来验证消息确实是由发送方发出的,以及在传输过程中没有被篡改过。
JWT 的优点包括:
- 无状态:
JWT
本身就包含了用户信息,不需要再去查询数据库或者其他的存储设备。 - 安全性:由于
JWT
包含了签名,所以一旦JWT
被篡改,接收方就能够检测到。 - 便捷性:
JWT
的格式是轻量级的,容易传输,可以通过URL
、POST
参数或者在HTTP header
中传递。
使用 JWT
的过程可以分为以下几个步骤:
- 在服务器端生成一个
JWT
,包括Header
、Payload
和Signature
。 - 客户端在需要验证用户身份的请求中,将
JWT
添加到HTTP header
中。 - 服务器收到请求后,从
HTTP header
中提取JWT
,并对其进行验证,包括签名验证、Payload
中的信息验证等。如果验证成功,服务器返回请求所需的数据。如果验证失败,则拒绝请求。
二、go生成jwt
package main
import (
"github.com/golang-jwt/jwt/v4"
"time"
)
const (
ExpireDuration = 3600 * time.Second
JwtSecretKey = "abc123"
)
type MyClaims struct {
Id int64 `json:"id"`
Username string `json:"username"`
jwt.StandardClaims
}
// 生成token
func GenerateToken(id int64, username string) (string, error) {
// 定义token的过期时间
expireTime := time.Now().Add(ExpireDuration).Unix()
// 创建一个自定义的Claims
myClaims := &MyClaims{
Id: id,
Username: username,
StandardClaims: jwt.StandardClaims{
Audience: "",
ExpiresAt: expireTime,
Id: "",
IssuedAt: time.Now().Unix(),
Issuer: "lym",
NotBefore: 0,
Subject: "",
},
}
// 使用 JWT 签名算法生成token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, myClaims)
// 将token进行加盐加密
// 注意:该方法参数虽然是interface{},但是必须传入[]byte类型的参数
tokenString, err := token.SignedString([]byte(JwtSecretKey))
if err != nil {
return "", err
}
return tokenString, nil
}
详细分析:
type MyClaims struct {
Id int64 `json:"id"`
Username string `json:"username"`
jwt.StandardClaims
}
该结构体用于定义 JWT
的 payload
,也就是 JWT
载荷中存储的信息。其中,Id
和 Username
用于标识用户身份,jwt.StandardClaims
则是 JWT
的标准声明,包含了 JWT
的一些基本信息,比如过期时间、签发时间等。在创建 JWT
时,我们需要将这个结构体传入,以便生成 JWT
的 payload
。在验证 JWT
时,我们也需要解析出这个结构体,以便获取 JWT
中存储的用户身份信息。
StandardClaims中
的信息用于控制JWT token
的生命周期和有效性。
ExpiresAt
表示JWT token
的过期时间,超过这个时间后,JWT token
将失效,无法再被使用。IssuedAt
表示JWT token
的签发时间,Issuer
表示JWT token
的签发者,这些信息可以帮助验证JWT token
的合法性。
注:在MyClaims结构体中通过匿名嵌入jwt.StandardClaims,便是让MyClaims继承了jwt.StandardClaims,从而实现了Claims接口,可以作为 jwt.NewWithClaims函数的第二个参数,如下:
type Claims interface {
Valid() error
}
// NewWithClaims creates a new Token with the specified signing method and claims.
func NewWithClaims(method SigningMethod, claims Claims) *Token {
return &Token{
Header: map[string]interface{}{
"typ": "JWT",
"alg": method.Alg(),
},
Claims: claims,
Method: method,
}
}
另外一种常见的方式是直接使用jwt.MapClaims
,它的定义为type MapClaims map[string]interface{}
,也实现了Claims
接口,使用方式如下
// 生成token
func GenerateToken(id int64, username string) (string, error) {
// 使用 JWT 签名算法生成token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"id": id,
"username": username,
})
// 将token进行加盐加密
tokenString, err := token.SignedString(JwtSecretKey)
if err != nil {
return "", err
}
return tokenString, nil
}
为何要加盐加密?
加盐加密的目的是为了增加数据的安全性,防止攻击者对加密后的数据进行破解。加盐是指在原始数据的基础上加上一些随机的字符串或数字,使得同样的原始数据加盐后的结果是不同的。加密是指将原始数据和盐一起通过某种加密算法进行加密,得到一串密文。密文是不能被破解的,只能通过使用同样的盐和加密算法对原始数据进行加密后得到相同的密文来验证数据的真实性。
在JWT
中,加盐加密可以有效地防止攻击者伪造token
,从而保障应用的安全性。具体地,JWT
在生成token
时会使用指定的秘钥对payload
进行签名,这个秘钥就是加盐的一部分。只有使用相同的秘钥才能对payload
进行解密并验证token
的真实性。因此,只有知道秘钥的人才能生成有效的token
,从而有效地保护了应用的安全性。
jwt中给token加盐的方法有一个注意点:必须传入[]byte类型的参数
// 注意:该方法参数虽然是interface{},但是必须传入[]byte类型的参数
tokenString, err := token.SignedString([]byte(JwtSecretKey))
原因:
进入SignedString()
的源码,可以看出SignedString
使用SigningMethodHS256
方式,结合一个随机值(JwtSecretKey)
进行加密,进入t.Method.Sign(sstr, key)
中查看,找到SigningMethodHS256
所属的SigningMethodHMAC
类型函数:
// Implements the Sign method from SigningMethod for this signing method.
// Key must be []byte
func (m *SigningMethodHMAC) Sign(signingString string, key interface{}) (string, error) {
if keyBytes, ok := key.([]byte); ok {
if !m.Hash.Available() {
return "", ErrHashUnavailable
}
hasher := hmac.New(m.Hash.New, keyBytes)
hasher.Write([]byte(signingString))
return EncodeSegment(hasher.Sum(nil)), nil
}
return "", ErrInvalidKeyType
}
注释中明确说明key必须是[]byte类型。代码中也是只针对[]byte类型进行处理:key.([]byte)。
单元测试
package main
import "testing"
func TestGenerateToken(t *testing.T) {
token, err := GenerateToken(1, "lym")
if err != nil {
t.Fatal(err)
}
t.Logf(token)
}
运行之后输出:
稍后验证解析token
时就可以直接用本次生成的
=== RUN TestGenerateToken
main_test.go:10: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJseW0iLCJleHAiOjE3MDEwMTgzMTMsImlhdCI6MTcwMTAxNDcxMywiaXNzIjoibHltIn0.0YeCw4Nxl7QCXtt_7YaXXVwaUu7UJbGoMAJaEZDYZTA
--- PASS: TestGenerateToken (0.00s)
PASS
Process finished with the exit code 0
三、go解析jwt
func ParseToken(tokenString string) (*MyClaims, error) {
// 解析 token
token, err := jwt.ParseWithClaims(tokenString, &MyClaims{}, func(token *jwt.Token) (interface{}, error) {
// 注意:虽然返回值是interface,但是这里必须返回[]byte类型,否则运行时会报错key is of invalid type
return []byte(JwtSecretKey), nil
})
if err != nil {
return nil, err
}
if myClaims, ok := token.Claims.(*MyClaims); ok && token.Valid {
return myClaims, nil
} else {
return nil, jwt.NewValidationError("invalid token", jwt.ValidationErrorClaimsInvalid)
}
}
jwt.ParseWithClaims()
函数用于解析并验证JWT token
,并提取出其中的MyClaims
数据结构。该函数接受三个参数:
tokenString
: 要解析的JWT token
字符串。claims
: 一个结构体指针,用于接收从JWT token
中解析出来的Claims
数据,赋值给返回值token
的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 // The raw token. Populated when you Parse a token
Method SigningMethod // The signing method used or to be used
Header map[string]interface{} // The first segment of the token
Claims Claims // The second segment of the token
Signature string // The third segment of the token. Populated when you Parse a token
Valid bool // Is the token valid? Populated when you Parse/Verify a token
}
keyFunc
: 一个回调函数,用于验证JWT token
的签名。在函数内部,可以对JWT token
签名进行验证,比如检查签名是否正确、是否过期等等。该函数需要返回一个interface{}
类型的值,表示用于验证签名的密钥。如果验证成功,应该返回一个非空的密钥;如果验证失败,应该返回一个nil
值和相应的错误信息。
注意:虽然keyFunc
第一个返回值是interface{}
,但是这里必须返回[]byte
类型,否则运行时会报错key is of invalid type
// 解析 token
token, err := jwt.ParseWithClaims(tokenString, &MyClaims{}, func(token *jwt.Token) (interface{}, error) {
// 注意:虽然返回值是interface{},但是这里必须返回[]byte类型,否则运行时会报错key is of invalid type
return []byte(JwtSecretKey), nil
})
在上述代码中,&MyClaims{}
作为第二个参数传递给了jwt.ParseWithClaims()
函数,这表示从JWT token
中解析出的Claims
数据将被解码为该结构体类型。第三个参数是一个匿名函数,用于验证JWT token
的签名,JwtSecretKey
被作为用于验证签名的密钥传递。如果JWT token
验证通过,jwt.ParseWithClaims()
函数将返回一个*jwt.Token
类型的指针,其中包含了解码后的Claims
数据,以及一些其他的元数据。如果验证失败,函数将返回相应的错误信息。
单元测试:
func TestParseToken(t *testing.T) {
c, err := ParseToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJseW0iLCJleHAiOjE3MDEwMTgzMTMsImlhdCI6MTcwMTAxNDcxMywiaXNzIjoibHltIn0.0YeCw4Nxl7QCXtt_7YaXXVwaUu7UJbGoMAJaEZDYZTA")
if err != nil {
t.Fatal(err)
}
t.Log(*c)
}
运行后输出:
=== RUN TestParseToken
main_test.go:18: {1 lym { 1701018313 1701014713 lym 0 }}
--- PASS: TestParseToken (0.00s)
PASS
Process finished with the exit code 0
四、总结
JWT(JSON Web Token)
是一种用于身份验证的开放标准,它通过在用户和服务器之间传递被加密的 JSON
对象来安全地传输信息。JWT
由三个部分组成:头部、载荷和签名。其中,头部包含加密算法和 token
类型等信息,载荷包含存储在token
中的用户信息,签名用于验证token
的真实性和完整性。
使用 JWT 时需要注意以下几点:
- 避免在
token
中存储敏感信息,比如密码、银行卡号等。 - 需要使用安全的算法来签名和加密
token
,比如使用HMAC
或RSA
加密。 token
中包含的用户信息不应该过多,只保留必要的信息即可。- 在使用
JWT
进行身份验证时,需要防止令牌被盗用。可以采用一些技术手段,比如限制令牌的有效期、限制令牌的使用次数等。 - 在生成
token
时需要注意加盐加密,以提高安全性。 - 在解析
token
时需要对token
进行校验,以保证token
的真实性和完整性。 - 最后,需要注意遵循安全的开发实践,保障应用的安全性。