JWT 身份验证:构建安全的 API 认证系统
在构建现代 Web API 时,身份验证是不可或缺的一环。传统的 Session 认证方式在分布式系统中面临诸多挑战,而 JWT(JSON Web Token)提供了一种优雅、无状态的解决方案。
本文将深入探讨 JWT 的原理、实现和最佳实践,帮你构建安全可靠的认证系统。
什么是 JWT?
JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间安全地传输声明信息。它是一个经过数字签名的 JSON 对象,可以被验证和信任。
JWT 的结构
一个 JWT 由三部分组成,用点号(.)分隔:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMjMsInVzZXJuYW1lIjoiYWxpY2UifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
|____________header____________|.|_______________________payload_______________________|.|___________________signature___________________|
1. Header(头部)
{
"alg": "HS256",
"typ": "JWT"
}
alg:签名算法(HS256、RS256 等)typ:Token 类型(固定为 JWT)
2. Payload(负载)
{
"user_id": 123,
"username": "alice",
"exp": 1516239022
}
包含声明(claims):
- 注册声明:iss(签发者)、exp(过期时间)、sub(主题)等
- 公共声明:自定义的声明
- 私有声明:双方约定的声明
3. Signature(签名)
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
用于验证消息的完整性和真实性。
Session vs JWT
Session 认证
客户端 → 登录 → 服务器(创建 session,返回 session_id)
客户端 ← Set-Cookie: session_id=abc123 ← 服务器
客户端 → 请求(携带 Cookie)→ 服务器(查询 session)
客户端 ← 响应 ← 服务器
优点:
- 服务器完全控制 session
- 可以随时使 session 失效
缺点:
- 需要服务器存储 session
- 分布式系统需要共享 session
- 扩展性受限
JWT 认证
客户端 → 登录 → 服务器(验证凭证,签发 JWT)
客户端 ← JWT ← 服务器
客户端 → 请求(携带 JWT)→ 服务器(验证 JWT,无需查询数据库)
客户端 ← 响应 ← 服务器
优点:
- 无状态,服务器不需要存储
- 易于水平扩展
- 跨域友好
- 适合微服务架构
缺点:
- Token 无法主动失效(需要额外机制)
- Token 可能较大
- 需要保护好密钥
使用 golang-jwt 库
go get github.com/golang-jwt/jwt/v5
基础实现
生成 JWT
package main
import (
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
)
// Claims 自定义声明
type Claims struct {
UserID int `json:"user_id"`
Username string `json:"username"`
jwt.RegisteredClaims
}
var jwtSecret = []byte("your-secret-key") // 生产环境使用环境变量
// GenerateToken 生成 JWT
func GenerateToken(userID int, username string) (string, error) {
claims := Claims{
UserID: userID,
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "myapp",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtSecret)
}
func main() {
token, err := GenerateToken(123, "alice")
if err != nil {
panic(err)
}
fmt.Println("Token:", token)
}
验证 JWT
// ParseToken 解析和验证 JWT
func ParseToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
// 验证签名算法
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return jwtSecret, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, fmt.Errorf("invalid token")
}
func main() {
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
claims, err := ParseToken(token)
if err != nil {
fmt.Println("Invalid token:", err)
return
}
fmt.Printf("User ID: %d, Username: %s\n", claims.UserID, claims.Username)
}
JWT 中间件
package main
import (
"context"
"net/http"
"strings"
)
type contextKey string
const userContextKey contextKey = "user"
// JWTMiddleware JWT 认证中间件
func JWTMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 从 Authorization 头获取 token
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "Missing authorization header", http.StatusUnauthorized)
return
}
// 验证 Bearer 格式
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
http.Error(w, "Invalid authorization header format", http.StatusUnauthorized)
return
}
tokenString := parts[1]
// 解析 token
claims, err := ParseToken(tokenString)
if err != nil {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
// 将用户信息添加到 context
ctx := context.WithValue(r.Context(), userContextKey, claims)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// GetUserFromContext 从 context 获取用户信息
func GetUserFromContext(r *http.Request) *Claims {
if claims, ok := r.Context().Value(userContextKey).(*Claims); ok {
return claims
}
return nil
}
完整的认证系统
package main
import (
"encoding/json"
"log"
"net/http"
"sync"
"time"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/crypto/bcrypt"
)
type User struct {
ID int `json:"id"`
Username string `json:"username"`
Password string `json:"-"` // 不序列化密码
}
type LoginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
type LoginResponse struct {
Token string `json:"token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int `json:"expires_in"`
}
type UserStore struct {
mu sync.RWMutex
users map[string]*User // username -> user
}
func NewUserStore() *UserStore {
return &UserStore{
users: make(map[string]*User),
}
}
var (
store = NewUserStore()
jwtSecret = []byte("your-secret-key")
refreshSecret = []byte("your-refresh-secret-key")
)
// 注册
func registerHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req LoginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
// 检查用户名是否已存在
store.mu.RLock()
if _, exists := store.users[req.Username]; exists {
store.mu.RUnlock()
http.Error(w, "Username already exists", http.StatusConflict)
return
}
store.mu.RUnlock()
// 哈希密码
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
// 创建用户
user := &User{
ID: len(store.users) + 1,
Username: req.Username,
Password: string(hashedPassword),
}
store.mu.Lock()
store.users[req.Username] = user
store.mu.Unlock()
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]string{"message": "User registered successfully"})
}
// 登录
func loginHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req LoginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
// 查找用户
store.mu.RLock()
user, exists := store.users[req.Username]
store.mu.RUnlock()
if !exists {
http.Error(w, "Invalid credentials", http.StatusUnauthorized)
return
}
// 验证密码
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
http.Error(w, "Invalid credentials", http.StatusUnauthorized)
return
}
// 生成 Access Token(短期)
accessToken, err := generateAccessToken(user)
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
// 生成 Refresh Token(长期)
refreshToken, err := generateRefreshToken(user)
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
response := LoginResponse{
Token: accessToken,
RefreshToken: refreshToken,
ExpiresIn: 3600, // 1 小时
}
json.NewEncoder(w).Encode(response)
}
func generateAccessToken(user *User) (string, error) {
claims := Claims{
UserID: user.ID,
Username: user.Username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "myapp",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtSecret)
}
func generateRefreshToken(user *User) (string, error) {
claims := jwt.RegisteredClaims{
Subject: string(rune(user.ID)),
ExpiresAt: jwt.NewNumericDate(time.Now().Add(7 * 24 * time.Hour)), // 7 天
IssuedAt: jwt.NewNumericDate(time.Now()),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(refreshSecret)
}
// 刷新 Token
func refreshTokenHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
RefreshToken string `json:"refresh_token"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
// 验证 refresh token
token, err := jwt.ParseWithClaims(req.RefreshToken, &jwt.RegisteredClaims{}, func(token *jwt.Token) (interface{}, error) {
return refreshSecret, nil
})
if err != nil {
http.Error(w, "Invalid refresh token", http.StatusUnauthorized)
return
}
claims, ok := token.Claims.(*jwt.RegisteredClaims)
if !ok || !token.Valid {
http.Error(w, "Invalid refresh token", http.StatusUnauthorized)
return
}
// 查找用户
store.mu.RLock()
var user *User
for _, u := range store.users {
if string(rune(u.ID)) == claims.Subject {
user = u
break
}
}
store.mu.RUnlock()
if user == nil {
http.Error(w, "User not found", http.StatusNotFound)
return
}
// 生成新的 access token
newAccessToken, err := generateAccessToken(user)
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(map[string]interface{}{
"token": newAccessToken,
"expires_in": 3600,
})
}
// 受保护的 API
func profileHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
claims := GetUserFromContext(r)
if claims == nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
store.mu.RLock()
user := store.users[claims.Username]
store.mu.RUnlock()
json.NewEncoder(w).Encode(user)
}
func main() {
mux := http.NewServeMux()
// 公开路由
mux.HandleFunc("/register", registerHandler)
mux.HandleFunc("/login", loginHandler)
mux.HandleFunc("/refresh", refreshTokenHandler)
// 受保护的路由
protectedMux := http.NewServeMux()
protectedMux.HandleFunc("/profile", profileHandler)
mux.Handle("/api/", JWTMiddleware(protectedMux))
log.Println("Server starting on :8080")
http.ListenAndServe(":8080", mux)
}
安全最佳实践
1. 使用强密钥
// ❌ 弱密钥
var jwtSecret = []byte("secret")
// ✅ 强密钥(至少 32 字节)
var jwtSecret = []byte("your-very-long-and-random-secret-key-at-least-32-bytes")
2. 设置合理的过期时间
// Access Token:短期(15 分钟 - 1 小时)
accessTokenClaims := Claims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * time.Minute)),
}
// Refresh Token:长期(7 天 - 30 天)
refreshTokenClaims := Claims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(7 * 24 * time.Hour)),
}
3. 使用 HTTPS
JWT 在传输过程中可能被截获,必须使用 HTTPS:
// 生产环境
http.ListenAndServeTLS(":443", "cert.pem", "key.pem", handler)
4. 不在 Token 中存储敏感信息
// ❌ 不要存储敏感信息
type Claims struct {
Password string `json:"password"` // 危险!
CreditCard string `json:"credit_card"` // 危险!
}
// ✅ 只存储必要的标识信息
type Claims struct {
UserID int `json:"user_id"`
Role string `json:"role"`
}
5. 验证签名算法
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
// 确保使用预期的算法
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return jwtSecret, nil
})
6. 实现 Token 黑名单
对于需要主动使 Token 失效的场景:
type TokenBlacklist struct {
mu sync.RWMutex
tokens map[string]time.Time // token -> expiration
}
var blacklist = &TokenBlacklist{
tokens: make(map[string]time.Time),
}
// 添加 token 到黑名单
func (b *TokenBlacklist) Add(token string, exp time.Time) {
b.mu.Lock()
defer b.mu.Unlock()
b.tokens[token] = exp
// 清理过期条目
go b.cleanup()
}
func (b *TokenBlacklist) cleanup() {
time.Sleep(1 * time.Hour)
b.mu.Lock()
defer b.mu.Unlock()
now := time.Now()
for token, exp := range b.tokens {
if now.After(exp) {
delete(b.tokens, token)
}
}
}
// 检查 token 是否在黑名单中
func (b *TokenBlacklist) Contains(token string) bool {
b.mu.RLock()
defer b.mu.RUnlock()
_, exists := b.tokens[token]
return exists
}
// 在中间件中使用
func JWTMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tokenString := extractToken(r)
if blacklist.Contains(tokenString) {
http.Error(w, "Token revoked", http.StatusUnauthorized)
return
}
// 继续验证...
})
}
总结
JWT 是现代 Web API 认证的主流方案,它提供了无状态、可扩展的认证机制。
关键要点:
- 理解 JWT 结构:Header、Payload、Signature
- 使用双 Token 机制:短期 Access Token + 长期 Refresh Token
- 保护好密钥:使用强密钥,不要硬编码
- 设置合理过期时间:Access Token 短期,Refresh Token 长期
- 验证签名算法:防止算法混淆攻击
- 使用 HTTPS:防止 Token 被截获
- 考虑 Token 黑名单:支持主动失效场景
记住:安全是一个持续的过程,要定期审查和更新你的认证系统。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。