Skip to content

7.7 JWT:原理与在 Go 中的使用

简介

JWT(JSON Web Token)是一种基于 JSON 的开放标准(RFC 7519),用于在各方之间安全地传递信息。在 Web 接口里常见用法是:用户登录后由服务端颁发 JWT,客户端在后续请求中携带,服务端验签并检查声明即可,不必每次为 Session 查库(具体是否查库仍取决于业务)。

特点:

  • 紧凑(Compact):可通过 URL、POST 参数或 HTTP 头传输。
  • 自包含(Self-contained):载荷里可带用户标识、角色、过期时间等声明(Claims)。
  • 可验证(Verifiable):签名保证 Header、Payload 未被篡改。

JWT 的结构

JWT 由三部分用 . 连接(最常见的 JWS 紧凑形式):

text
Base64Url(Header) . Base64Url(Payload) . Signature

写成「三段字符串」时即:

text
xxxxxxx.yyyyyyy.zzzzzzz
部分用途示例含义
Header指定算法与类型{"alg":"HS256","typ":"JWT"}
Payload存放声明(Claims),如用户 ID、过期时间{"sub":"…","name":"Alice","exp":…}
Signature保证前两段未被篡改见下文「HS256 的签名与验证」

注意: Header 与 Payload 都会先做 Base64URL 编码后再参与签名;第三段是签名结果再编码。Payload 默认不加密,仅编码,勿存放密码等敏感明文。

校验思路: 用同一算法与密钥对 Base64Url(Header) + "." + Base64Url(Payload) 重算签名,与第三段比对;一致且 exp 等声明合法,即认为 Token 可信。

密钥(Secret)

在使用 对称算法(如 HS256 / HS384 / HS512)时,Secret 同时用于签发验证 Token。

  • 应使用高强度随机字节,建议长度 ≥ 256 bit(32 字节)
  • 常见存放形式包括 URL-Safe Base64Hex 等;生产环境应来自安全配置或密钥管理服务(如 Vault、云 KMS),不要硬编码在仓库里

常见签名算法(alg)

算法说明
HS256HMAC + SHA-256,对称(共享 Secret)
HS384HMAC + SHA-384
HS512HMAC + SHA-512
RS256RSA + SHA-256,非对称(私钥签发、公钥验签)
ES256ECDSA + SHA-256(椭圆曲线)

小项目、单服务常用 HS256;对安全隔离要求高时可选 RS256 等。

在 Web 里怎么用(工作流程)

  1. 用户登录(如提交用户名、密码)。
  2. 服务端校验通过后签发 JWT
  3. 客户端保存 Token(如 LocalStorage、Cookie 等,按安全策略选择)。
  4. 后续请求携带 JWT(推荐请求头 Authorization: Bearer <token>)。
  5. 服务端验证签名,并检查声明(是否过期、角色是否足够等)。
  6. 验证通过则返回业务数据;否则返回 401 Unauthorized

交互示意: 用户把账号密码交给客户端 → 客户端 POST /login → 服务端返回 Header.Payload.Signature → 客户端在后续请求中带 Authorization: Bearer <JWT> → 服务端校验通过后返回受保护资源(如 200 + 用户信息)。

HS256 的签名与验证

签发:H = Base64Url(Header)P = Base64Url(Payload)Secret 为服务端密钥,则签名由算法定义(如 HMACSHA256(H + "." + P, Secret)),再对结果做 Base64URL 得到第三段。

验证:

  1. . 拆成 Header、Payload、Signature 三段。
  2. 用相同 Secret 与算法重算签名。
  3. 与第三段对比:相同则未被篡改;不同则拒绝访问。

优缺点

优点缺点
易做无状态扩展,减轻会话存储压力**无法单靠 JWT 本身「即时撤销」**未过期 Token,需短过期、黑名单或轮换密钥等策略
声明自包含,可减少每次请求查库Token 泄露影响大,需 HTTPS、谨慎存前端
适合跨域、微服务、移动端等场景Payload 可读,敏感信息勿明文写入

JWT 与 Session 对比

JWTSession 认证是 Web 里最常见的两种用户认证方式。二者在流程、数据放哪、扩展方式、安全与性能上都有明显差异;下面分点说明,文末附速览表便于对照。

认证流程

Session 认证

  1. 用户登录:用户在客户端输入用户名和密码,发送到服务器进行验证。
  2. 验证成功:服务器验证用户凭证无误后,在服务器端创建一个 Session 对象,该对象包含用户相关的信息(如用户 ID、权限等),并为这个 Session 生成一个唯一的 Session ID。
  3. 返回响应:服务器将 Session ID 通过 Cookie 发送给客户端。客户端后续的每一次请求,都会在请求头或者 Cookie 中带上这个 Session ID。
  4. 服务端验证:服务器接收到请求后,通过 Session ID 在服务器端查找对应的 Session 对象,确认用户身份和权限。如果 Session 存在且有效,则处理请求;否则,返回未认证错误。

JWT 认证

  1. 用户登录:同样由客户端提交凭证,服务端校验。
  2. 验证成功:服务端根据用户信息生成 JWT(Header、Payload、Signature);Payload 中常含用户 ID、角色、exp 等声明。
  3. 返回响应:将 JWT 返回客户端;客户端可存于 LocalStorage、Cookie、内存等(按安全策略选择)。
  4. 后续请求:多在请求头 Authorization: Bearer <token> 中携带 JWT。
  5. 服务端验证:取出 JWT,用密钥验签;通过后再解析 Payload 核对声明;不通过则返回未认证错误。

流程上的核心差异: Session 依赖服务端保存的 Session 与客户端传来的 Session ID 是否一致;JWT 则依赖对 Token 的签名校验声明解析,默认不必每次查「会话表」(业务上仍可配合黑名单、用户表等)。

数据存储位置

  • Session:认证相关数据(Session 对象)在服务端,需内存、Redis、数据库等存储;用户增多时,存储与维护压力上升。
  • JWT:用户信息主要在 Token 自身中;纯无状态场景下服务端不必为每个会话持久化一份副本,多实例无需共享 Session 存储,系统间耦合更低。

扩展性

  • Session:在分布式或微服务里,往往需要 Session 共享(集中式缓存、会话复制、粘性会话等),架构与运维成本更高。
  • JWT:各节点用同一密钥或公钥即可独立验签,横向扩展相对容易;需同时考虑密钥分发、吊销与轮换(见前文「优缺点」「安全注意事项」)。

安全性

  • Session:Session ID 若在 Cookie 中,被窃取(如 XSS)后攻击者可冒充用户;服务端保存的会话数据也需防篡改与越权访问。
  • JWT:没有签名密钥则难以篡改载荷内容;但若 Token 整串泄露(例如 LocalStorage 被恶意脚本读取),攻击者可直接重放请求,危害与 Session 被盗类似。常见 JWS 的 Payload 仅为 Base64 可读,勿存密码等敏感明文;更高要求时可采用 JWE 等加密方案,或 短过期 + 刷新令牌 降低泄露窗口。

性能

  • Session:每次请求往往要 查存储(缓存/数据库),用户量大时对延迟与吞吐有影响。
  • JWT:以本地验签为主,通常少一次中心化会话查询(若不做黑名单等业务仍可完全无查库)。代价是 Token 体积一般大于 Session ID,网络传输略增。

速览表

维度SessionJWT
状态服务端存会话,依赖 Cookie 等携带 Session ID常称「无状态」:验签通过即可信任声明(若查黑名单/用户表则仍有状态)
扩展多机通常要共享 Session 存储多实例独立验签,扩展相对容易;需注意密钥与吊销策略
撤销删除或失效 Session 即可未过期前默认仍有效,需 短过期、刷新令牌、黑名单或轮换密钥

安全注意事项

  • 生产环境务必 HTTPS,防中间人窃听 Token。
  • Secret 足够长且随机;把生产密钥写进代码仓库。
  • 设置合理 exp,关键操作可二次校验(如再次验证密码、短信等)。
  • Payload 可被解码阅读,勿存密码、隐私明文
  • 定期轮换 Secret,缩小泄露后的风险窗口。

golang-jwt:安装与基础用法

社区常用 github.com/golang-jwt/jwt。安装 v5

bash
go get github.com/golang-jwt/jwt/v5

下面先给出最小示例(签发 / 解析)与 Bearer 读取片段,均使用 HS256带 HTTP 的可运行示例实战:HTTP 登录与鉴权

随机 Secret 的生成

与「≥32 字节随机」建议一致,演示用 crypto/rand 生成后做 Base64URL 编码;演示可打印一次后固定使用,生产应从环境变量或密钥服务读取。

go
package main

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)
    }
    return base64.RawURLEncoding.EncodeToString(b)
}

签发 Token

go
package main

import (
    "fmt"
    "time"

    "github.com/golang-jwt/jwt/v5"
)

var hmacSampleSecret = []byte("请换成足够长且随机的密钥-仅作演示")

func main() {
    claims := jwt.MapClaims{
        "sub":  "user-1001",
        "name": "Alice",
        "exp":  time.Now().Add(24 * time.Hour).Unix(),
        "iat":  time.Now().Unix(),
    }

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    tokenString, err := token.SignedString(hmacSampleSecret)
    if err != nil {
        panic(err)
    }
    fmt.Println(tokenString)
}

解析与校验 Token

go
package main

import (
    "fmt"

    "github.com/golang-jwt/jwt/v5"
)

var hmacSampleSecret = []byte("请换成足够长且随机的密钥-仅作演示")

func main() {
    tokenString := "把上面打印出的整串 JWT 贴在这里"

    token, err := jwt.Parse(tokenString, 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 hmacSampleSecret, nil
    })
    if err != nil {
        fmt.Println("parse error:", err)
        return
    }

    if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
        fmt.Println("sub:", claims["sub"])
        fmt.Println("name:", claims["name"])
    } else {
        fmt.Println("invalid token")
    }
}

jwt.Parse 会校验签名与 exp(若存在);过期会返回错误。

从请求头读取 Bearer Token

go
import (
    "net/http"
    "strings"

    "github.com/golang-jwt/jwt/v5"
)

func bearerToken(r *http.Request) string {
    h := r.Header.Get("Authorization")
    if len(h) > 7 && strings.EqualFold(h[:7], "Bearer ") {
        return strings.TrimSpace(h[7:])
    }
    return ""
}

// 在 Handler 中:raw := bearerToken(r); 再 jwt.Parse(raw, keyFunc)

实战:HTTP 登录与鉴权

以下演示 /login/profile:登录成功返回 JSON 中的 token,访问受保护路由时在请求头携带 Authorization: Bearer …。与专栏思路一致;仅为教学演示,生产环境应使用 POST + JSON 登录并对接真实用户库校验。

go
package main

import (
    "fmt"
    "log"
    "net/http"
    "strings"
    "time"

    "github.com/golang-jwt/jwt/v5"
)

var secret = []byte("请换成 generateSecret() 或环境变量读取的密钥-仅作演示")

func createToken(username string) (string, error) {
    claims := jwt.MapClaims{
        "sub":  username,
        "name": "Alice",
        "iat":  time.Now().Unix(),
        "exp":  time.Now().Add(time.Hour).Unix(),
    }
    t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return t.SignedString(secret)
}

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 secret, nil
    })
}

func main() {
    http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
        user, pass := r.URL.Query().Get("user"), r.URL.Query().Get("pass")
        if user == "alice" && pass == "123" {
            tok, err := createToken(user)
            if err != nil {
                http.Error(w, "签发失败", http.StatusInternalServerError)
                return
            }
            w.Header().Set("Content-Type", "application/json")
            fmt.Fprintf(w, `{"token":"%s"}`, tok)
            return
        }
        http.Error(w, "用户名或密码错误", http.StatusUnauthorized)
    })

    http.HandleFunc("/profile", func(w http.ResponseWriter, r *http.Request) {
        auth := r.Header.Get("Authorization")
        parts := strings.SplitN(auth, " ", 2)
        if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") {
            http.Error(w, "缺少或无效的 Authorization 头", http.StatusUnauthorized)
            return
        }
        token, err := verifyToken(strings.TrimSpace(parts[1]))
        if err != nil || !token.Valid {
            http.Error(w, "Token 无效或已过期", http.StatusUnauthorized)
            return
        }
        claims := token.Claims.(jwt.MapClaims)
        w.Header().Set("Content-Type", "application/json")
        fmt.Fprintf(w, `{"user":"%v","name":"%v"}`, claims["sub"], claims["name"])
    })

    log.Println("监听 http://localhost:8080  示例: GET /login?user=alice&pass=123  再带 Bearer 访问 /profile")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

小结

  • JWT 是 Header.Payload.Signature,Payload 默认仅编码不加密;验签防篡改,exp 等声明控制有效期。
  • 对称算法依赖强 SecretRS256 等适合私钥签、公钥验的场景。
  • Session 的差异(流程、存储位置、扩展、安全、性能)见上文 「JWT 与 Session 对比」
  • Go 使用 github.com/golang-jwt/jwt/v5 即可签发与解析;生产环境注意 HTTPS、密钥管理、短过期与吊销策略

延伸阅读