🔐 一文读懂 JWT(JSON Web Token)
一、什么是 JWT?
- JWT(JSON Web Token)
一种基于 JSON 的开放标准(RFC 7519),用于在各方之间安全地传递信息。 - 特点
- 紧凑(Compact):可通过 URL、POST 参数或 HTTP 头传输
- 自包含(Self-contained):携带了用户的基本身份信息和声明(Claims)
- 可验证(Verifiable):签名保证数据未被篡改
📌 典型场景:用户登录后,服务器颁发一个 JWT,客户端每次请求都携带它,无需再查数据库做 Session 验证。
二、JWT 的三部分结构
一个典型的 JWT 长这样:
xxxxxxx.yyyyyyy.zzzzzzz
部分 | 用途 | 示例内容 |
---|---|---|
Header(头部) | 指定算法和类型 | {"alg":"HS256","typ":"JWT"} |
Payload(负载) | 存放声明(Claims),如用户 ID、过期时间等 | {"sub":"1234567890","name":"Alice","exp":1600000000} |
Signature(签名) | 保证前两部分不被篡改 | HMACSHA256(Base64Url(Header) + "." + Base64Url(Payload), Secret) |
✨ 注意:Header 和 Payload 都要做 Base64Url 编码!
JWT = Base64Url(Header) + "." + Base64Url(Payload) + "." + Base64Url(Signature)
三、JWT Secret(密钥)介绍 🔑
- 作用
- 对称签名(HS256/HS384/HS512)中,Secret 用来签发和验证 Token。
- 形式 & 长度
- 高强度随机字节,建议 ≥256 bits(32 bytes)
- 常见编码:
- URL-Safe Base64
9d6bXFMmZ3RV8Ytp9rz8QpKBuGV9zZ4T5vHSuJEjw8M
- Hex
e75ab5c53266774557c62da7dacfc429281b8695f736784f9bc74ae24923c3c30
- URL-Safe Base64
- 如何生成?
- Go 代码示例:
import ( "crypto/rand" "encoding/base64" "log" ) func generateSecret() string { b := make([]byte, 32) if _, err := rand.Read(b); err != nil { log.Fatalf("生成 Secret 失败:%v", err) } // RawURLEncoding 去掉末尾 = 号,更 URL-Safe return base64.RawURLEncoding.EncodeToString(b) }
- Go 代码示例:
- 泄露后果
- 攻击者可伪造任意合法 JWT,冒充用户或管理员
- 权限绕过、数据篡改、系统信任链崩溃
- 等同「根密钥」被盗,所有鉴权瞬间失效
- 防护 & 补救
- 安全存储:✨使用 Vault、AWS KMS、GCP Secret Manager 等
- 定期轮换(Key Rotation):支持「新」+「旧」秘钥平滑过渡
- 缩短生命周期:结合
exp
,让 Token & Secret 都有“有效期” - 多重验证:对支付、重置密码等关键操作再做一次二次校验
- 监控告警:异常签名、同账户多地登录及时告警并强制登出
四、签名算法(alg)
算法名称 | 描述 |
---|---|
HS256 | HMAC + SHA-256,对称加密(Shared Secret) |
HS384 | HMAC + SHA-384 |
HS512 | HMAC + SHA-512 |
RS256 | RSA + SHA-256,非对称加密(公钥/私钥对) |
ES256 | ECDSA + SHA-256,椭圆曲线数字签名算法 |
小项目常用 HS256;高安全需求可选 RS256(私钥签发、公钥验签)。
五、JWT 的工作流程
- 用户登录(提供用户名/密码)
- 服务器验证成功后,签发 JWT
- 客户端保存(LocalStorage / Cookie)
- 后续请求携带 JWT
- 推荐:请求头
Authorization: Bearer <token>
- 推荐:请求头
- 服务器 验证签名 & 检查声明(如是否过期、是否有权限)
- 验证通过,返回数据;否则
401 Unauthorized
六、签名 & 验证(HS256 示例)
1. 签名生成
令
H = Base64Url(Header)
P = Base64Url(Payload)
Secret = 服务器持有的密钥
LaTeX 公式版:
Signature=HMACSHA256(H+"."+P, Secret) \mathrm{Signature} = \mathrm{HMACSHA256}\bigl(H + "." + P,\ \mathrm{Secret}\bigr) Signature=HMACSHA256(H+"."+P, Secret)
2. 验证流程
- 拆分
Header.Payload.Signature
- 重新计算
HMACSHA256(H+"."+P, Secret)
- 对比结果:
- 相同:✅ 数据未被篡改
- 不同:❌ 拒绝访问
七、优缺点一览
优点 | 缺点 |
---|---|
1. 无状态(Stateless),可水平扩展 🌐 | 1. 无法即时「撤销」已签发的 Token |
2. 携带信息自包含,无需多次查询数据库 📦 | 2. Token 泄露风险大,需妥善存储 |
3. 支持跨域认证(适合微服务、移动端)📱🌍 | 3. Payload 明文可读,敏感信息请勿存放 |
八、实战示例(Go 语言版)
下面用 Go + 标准库 + github.com/golang-jwt/jwt/v4
库演示:
package main
import (
"crypto/rand"
"encoding/base64"
"fmt"
"log"
"net/http"
"strings"
"time"
"github.com/golang-jwt/jwt/v4"
)
// 1. 生成 Secret
func generateSecret() string {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
log.Fatalf("生成 Secret 失败:%v", err)
}
return base64.RawURLEncoding.EncodeToString(b)
}
// 全局 Secret(示例中硬编码,生产环境请用安全存储)
var SECRET = generateSecret()
// 2. 签发 JWT
func createToken(username string) (string, error) {
claims := jwt.MapClaims{
"sub": username,
"name": "Alice",
"iat": time.Now().Unix(),
"exp": time.Now().Add(time.Hour).Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(SECRET))
}
// 3. 验证 JWT
func verifyToken(tokenStr string) (*jwt.Token, error) {
return jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
return []byte(SECRET), nil
})
}
func main() {
http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
// 仅示例:实际应解析 JSON & 验证数据库
username, password := r.URL.Query().Get("user"), r.URL.Query().Get("pass")
if username == "alice" && password == "123" {
token, err := createToken(username)
if err != nil {
http.Error(w, "Token 签发失败", 500)
return
}
fmt.Fprintf(w, `{"token":"%s"}`, token)
return
}
http.Error(w, "用户名或密码错误", 401)
})
http.HandleFunc("/profile", func(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
parts := strings.SplitN(auth, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
http.Error(w, "缺少或无效的 Authorization 头", 401)
return
}
token, err := verifyToken(parts[1])
if err != nil || !token.Valid {
http.Error(w, "Token 无效或已过期", 401)
return
}
claims := token.Claims.(jwt.MapClaims)
fmt.Fprintf(w, `{"user":"%s","name":"%s"}`, claims["sub"], claims["name"])
})
log.Printf("🔑 Secret(演示用): %s\n", SECRET)
log.Println("🚀 Server 启动: http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
启动后:
- 登录:
GET http://localhost:8080/login?user=alice&pass=123 - 拿到
{"token":"..."}
- 请求受保护路由:
GET /profile HTTP/1.1 Host: localhost:8080 Authorization: Bearer <token>
九、总结 & 小贴士
- Secret 存储
- 生产环境切勿硬编码,使用专业密钥管理服务(Vault, AWS KMS…)
- Token 生命周期
- 设置合理
exp
,并对关键操作做二次校验
- 设置合理
- 安全细节
- Payload 明文可读,切勿存敏感数据
- 定期轮换 Secret,缩小泄露风险窗口
- 始终启用 HTTPS,防止中间人攻击
🎉 至此,你已经掌握了 JWT 的核心原理、结构、Secret 要点、使用流程及 Go 实战示例。快去项目中试一试吧!