JWT介绍
定义
JSON Web Token(JWT)是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间作为 JSON 对象安全地传输信息。由于该信息经过数字签名,因此可以被验证和信任。
结构
JWT的紧凑形式由三部分组成,每部分经过 Base64Url 编码后以点号( . )连接,分别是:
Header( 头部 )
包含令牌类型 ( typ ) 和签名算法 ( alg ) ,例如:1
2
3
4{
"alg": "HS256",
"typ": "JWT"
}该JSON经过Base64Url编码,形成 JWT 的第一部分( 如 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 )。
Payload( 载荷 )
包含声明 ( claims ) ,即需要传递的数据。声明分为三类:- Registered Claims(注册声明)(非强制但推荐使用):
- Public Claims(公共声明):可自定义,但需避免冲突(建议使用 IANA 注册或 URI 命名空间)。
- Private Claims(私有声明):双方协商的自定义数据。
示例:
1
2
3
4
5{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
该 JSON 会经过 Base64Url 编码,形成 JWT 的第二部分( 如 eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ )。签名仅防篡改,不加密!头部和载荷信息可被任何人解码查看,敏感数据需额外加密。
Signature( 签名 )
用于验证令牌完整性和真实性。生成方式依赖头部指定的算法:- HMAC SHA256 示例:
1
2
3
4HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secretKey
)- RSA 示例(使用私钥签名,公钥验证):
1
2
3
4RSASHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
privateKey
)签名结果经过 Base64Url 编码,形成 JWT 的第三部分(如 SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c)。
完整JWT示例
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ. SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
解码后
- Header: {“alg”:”HS256”,”typ”:”JWT”}
- Payload: {“sub”:”1234567890”,”name”:”John Doe”,”iat”:1516239022}
- Signature: 使用密钥验证前两部分是否被篡改
使用场景
Authorization ( 授权 )
JWT 最常见的用途。用户登录后,每个后续请求都会携带 JWT,允许用户访问与该令牌匹配的路由、服务和资源。如今,单点登录(Single Sign-On, SSO) 广泛使用 JWT,因为它开销小,且能轻松跨域使用。Information Exchange ( 信息交换 )
JWT 是在各方之间安全传输信息的一种优秀方式。由于 JWT 可以签名(例如使用公私钥对),因此可以确保发送者的身份真实可信。此外,由于签名是基于头部(header)和载荷(payload)计算的,还能验证内容是否被篡改。
工作原理
在身份验证过程中,当用户使用凭据成功登录后,系统会返回一个 JSON Web Token(JWT)。由于令牌本身就是凭证,必须格外注意防范安全问题。一般来说,令牌的保存时间不应超过必要期限。同时,由于浏览器存储缺乏安全性,不应在其中保存敏感的会话数据。
当用户需要访问受保护的路由或资源时,用户代理(如浏览器)应当发送 JWT,通常是通过 Authorization 头部使用 Bearer 模式进行传输。头部内容格式如下:
1 | Authorization: Bearer <token> |
在某些情况下,这可以成为一种无状态的授权机制。服务器的受保护路由会检查 Authorization 头部中的有效 JWT,如果存在有效令牌,则允许用户访问受保护资源。如果JWT包含必要数据,可能会减少某些操作对数据库查询的需求,不过实际情况可能有所不同。
在Go语言中使用JWT
添加 jwt-go 作为依赖项
1
go get -u github.com/golang-jwt/jwt/v5
导入至代码中
1
import "github.com/golang-jwt/jwt/v5"
定义安全密钥以及 Claims 结构体
1
2
3
4
5
6
7
8var secret = []byte("your secret here") // 从安全配置中读取
// 自定义JWT Claims结构体, 嵌入了标准Claims
type MyClaims struct {
UserID uint64 `json:"user_id"`
UserName string `json:"user_name"`
jwt.StandardClaims
}生成 Token 的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14func GenToken(user_id uint64, user_name string) (string, error) {
c := MyClaims{
UserID: user_id,
UserName: user_name,
StandardClaims: jwt.StandardClaims{
ExpiresAt: ***, // 过期时间,强制 Token 在一段时间后失效,减少泄露后的风险窗口
Issuer: ***, // 签发人,标识 Token 的签发主体(通常是服务端应用或授权服务器)
},
}
// 使用指定的签名方法创建签名对象
token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
// 使用指定的secret签名并获得完整的编码后的字符串token
return token.SignedString(secret)
}解析 Token 的函数
1
2
3
4
5
6
7
8
9
10
11
12func ParseToken(tokenString string) (*MyClaims, error) {
token, err := jwt.ParseWithClaims(tokenString, &MyClaims{}, func(t *jwt.Token) (interface{}, error) {
return secret, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*MyClaims); ok && token.Valid {
return claims, nil
}
return nil, errors.New("invalid token")
}实现基于 JWT 的 Gin 中间件,验证请求中的 Token 并提取用户信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60type contextKey string
const (
ContextUserIDKey contextKey = "userID"
)
func JWTAuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 1. 从多种位置获取 Token
tokenString := extractToken(c)
if tokenString == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Authorization token is required",
})
c.Abort()
return
}
// 2. 解析并验证 Token
claims, err := jwt.ParseToken(tokenString)
if err != nil {
log.Printf("JWT validation failed: %v", err)
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Invalid or expired token",
})
c.Abort()
return
}
// 3. 存储用户信息到上下文
c.Set(string(ContextUserIDKey), claims.UserID)
c.Next()
}
}
// 支持从 Header/Cookie/Query 提取 Token
func extractToken(c *gin.Context) string {
// 1. 从 Authorization Header 获取
authHeader := strings.TrimSpace(c.GetHeader("Authorization"))
if authHeader != "" {
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) == 2 && strings.EqualFold(parts[0], "Bearer") {
return parts[1]
}
}
// 2. 从 Query 参数获取
if token := c.Query("token"); token != "" {
return token
}
// 3. 从 Cookie 获取
if token, err := c.Cookie("token"); err == nil {
return token
}
return ""
}