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 紧凑形式):
Base64Url(Header) . Base64Url(Payload) . Signature写成「三段字符串」时即:
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 Base64、Hex 等;生产环境应来自安全配置或密钥管理服务(如 Vault、云 KMS),不要硬编码在仓库里。
常见签名算法(alg)
| 算法 | 说明 |
|---|---|
| HS256 | HMAC + SHA-256,对称(共享 Secret) |
| HS384 | HMAC + SHA-384 |
| HS512 | HMAC + SHA-512 |
| RS256 | RSA + SHA-256,非对称(私钥签发、公钥验签) |
| ES256 | ECDSA + SHA-256(椭圆曲线) |
小项目、单服务常用 HS256;对安全隔离要求高时可选 RS256 等。
在 Web 里怎么用(工作流程)
- 用户登录(如提交用户名、密码)。
- 服务端校验通过后签发 JWT。
- 客户端保存 Token(如 LocalStorage、Cookie 等,按安全策略选择)。
- 后续请求携带 JWT(推荐请求头
Authorization: Bearer <token>)。 - 服务端验证签名,并检查声明(是否过期、角色是否足够等)。
- 验证通过则返回业务数据;否则返回 401 Unauthorized。
交互示意: 用户把账号密码交给客户端 → 客户端 POST /login → 服务端返回 Header.Payload.Signature → 客户端在后续请求中带 Authorization: Bearer <JWT> → 服务端校验通过后返回受保护资源(如 200 + 用户信息)。
HS256 的签名与验证
签发: 令 H = Base64Url(Header),P = Base64Url(Payload),Secret 为服务端密钥,则签名由算法定义(如 HMACSHA256(H + "." + P, Secret)),再对结果做 Base64URL 得到第三段。
验证:
- 按
.拆成 Header、Payload、Signature 三段。 - 用相同
Secret与算法重算签名。 - 与第三段对比:相同则未被篡改;不同则拒绝访问。
优缺点
| 优点 | 缺点 |
|---|---|
| 易做无状态扩展,减轻会话存储压力 | **无法单靠 JWT 本身「即时撤销」**未过期 Token,需短过期、黑名单或轮换密钥等策略 |
| 声明自包含,可减少每次请求查库 | Token 泄露影响大,需 HTTPS、谨慎存前端 |
| 适合跨域、微服务、移动端等场景 | Payload 可读,敏感信息勿明文写入 |
JWT 与 Session 对比
JWT 与 Session 认证是 Web 里最常见的两种用户认证方式。二者在流程、数据放哪、扩展方式、安全与性能上都有明显差异;下面分点说明,文末附速览表便于对照。
认证流程
Session 认证
- 用户登录:用户在客户端输入用户名和密码,发送到服务器进行验证。
- 验证成功:服务器验证用户凭证无误后,在服务器端创建一个 Session 对象,该对象包含用户相关的信息(如用户 ID、权限等),并为这个 Session 生成一个唯一的 Session ID。
- 返回响应:服务器将 Session ID 通过 Cookie 发送给客户端。客户端后续的每一次请求,都会在请求头或者 Cookie 中带上这个 Session ID。
- 服务端验证:服务器接收到请求后,通过 Session ID 在服务器端查找对应的 Session 对象,确认用户身份和权限。如果 Session 存在且有效,则处理请求;否则,返回未认证错误。
JWT 认证
- 用户登录:同样由客户端提交凭证,服务端校验。
- 验证成功:服务端根据用户信息生成 JWT(Header、Payload、Signature);Payload 中常含用户 ID、角色、
exp等声明。 - 返回响应:将 JWT 返回客户端;客户端可存于 LocalStorage、Cookie、内存等(按安全策略选择)。
- 后续请求:多在请求头
Authorization: Bearer <token>中携带 JWT。 - 服务端验证:取出 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,网络传输略增。
速览表
| 维度 | Session | JWT |
|---|---|---|
| 状态 | 服务端存会话,依赖 Cookie 等携带 Session ID | 常称「无状态」:验签通过即可信任声明(若查黑名单/用户表则仍有状态) |
| 扩展 | 多机通常要共享 Session 存储 | 多实例独立验签,扩展相对容易;需注意密钥与吊销策略 |
| 撤销 | 删除或失效 Session 即可 | 未过期前默认仍有效,需 短过期、刷新令牌、黑名单或轮换密钥 等 |
安全注意事项
- 生产环境务必 HTTPS,防中间人窃听 Token。
- Secret 足够长且随机;勿把生产密钥写进代码仓库。
- 设置合理
exp,关键操作可二次校验(如再次验证密码、短信等)。 - Payload 可被解码阅读,勿存密码、隐私明文。
- 可定期轮换 Secret,缩小泄露后的风险窗口。
golang-jwt:安装与基础用法
社区常用 github.com/golang-jwt/jwt。安装 v5:
go get github.com/golang-jwt/jwt/v5下面先给出最小示例(签发 / 解析)与 Bearer 读取片段,均使用 HS256。带 HTTP 的可运行示例见 实战:HTTP 登录与鉴权。
随机 Secret 的生成
与「≥32 字节随机」建议一致,演示用 crypto/rand 生成后做 Base64URL 编码;演示可打印一次后固定使用,生产应从环境变量或密钥服务读取。
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
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
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
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 登录并对接真实用户库校验。
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 等声明控制有效期。
- 对称算法依赖强 Secret;RS256 等适合私钥签、公钥验的场景。
- 与 Session 的差异(流程、存储位置、扩展、安全、性能)见上文 「JWT 与 Session 对比」。
- Go 使用
github.com/golang-jwt/jwt/v5即可签发与解析;生产环境注意 HTTPS、密钥管理、短过期与吊销策略。