共计 2776 个字符,预计需要花费 7 分钟才能阅读完成。
背景痛点
在分布式系统中,API Gateway 作为流量的入口,承担着鉴权的重任。传统的鉴权方案通常采用数据库验证 token 的方式,这种方案在高并发场景下会面临以下问题:

- 数据库压力大:每个请求都需要查询数据库验证 token,导致数据库成为性能瓶颈
- 网络延迟高:数据库查询需要跨网络,增加了请求的响应时间
- 扩展性差:随着用户量增长,数据库难以水平扩展
技术选型
为了解决上述问题,我们对比了几种常见的鉴权方案:
- OAuth2.0
- 优点:标准化协议,支持多种授权模式
-
缺点:实现复杂,不适合内部服务鉴权
-
自定义 Token
- 优点:实现简单,完全可控
-
缺点:安全性难以保证,缺乏标准化
-
JWT + Redis
- 优点:无状态、可扩展、性能高
- 缺点:token 一旦签发无法撤销(可通过 Redis 缓存解决)
综合考虑后,我们选择了 JWT + Redis 的组合方案。JWT 提供无状态验证,Redis 缓存解决 token 撤销问题,同时提供高性能的验证能力。
核心实现
JWT Token 生成 / 验证
以下是使用 Go 1.21 实现的 JWT 生成和验证代码:
package auth
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"github.com/google/uuid"
)
type Claims struct {
UserID string `json:"uid"`
IssuedAt int64 `json:"iat"`
ExpiresAt int64 `json:"exp"`
TokenID string `json:"jti"`
}
func GenerateToken(secret []byte, userID string, ttl time.Duration) (string, error) {now := time.Now().Unix()
claims := Claims{
UserID: userID,
IssuedAt: now,
ExpiresAt: now + int64(ttl.Seconds()),
TokenID: uuid.New().String(),
}
header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"HS256","typ":"JWT"}`))
payload, err := json.Marshal(claims)
if err != nil {return "", fmt.Errorf("marshal claims failed: %w", err)
}
payloadEncoded := base64.RawURLEncoding.EncodeToString(payload)
signingInput := fmt.Sprintf("%s.%s", header, payloadEncoded)
h := hmac.New(sha256.New, secret)
h.Write([]byte(signingInput))
signature := base64.RawURLEncoding.EncodeToString(h.Sum(nil))
return fmt.Sprintf("%s.%s.%s", header, payloadEncoded, signature), nil
}
func VerifyToken(secret []byte, token string) (*Claims, error) {parts := strings.Split(token, ".")
if len(parts) != 3 {return nil, errors.New("invalid token format")
}
h := hmac.New(sha256.New, secret)
h.Write([]byte(fmt.Sprintf("%s.%s", parts[0], parts[1])))
signature := base64.RawURLEncoding.EncodeToString(h.Sum(nil))
if signature != parts[2] {return nil, errors.New("invalid signature")
}
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {return nil, fmt.Errorf("decode payload failed: %w", err)
}
var claims Claims
if err := json.Unmarshal(payload, &claims); err != nil {return nil, fmt.Errorf("unmarshal claims failed: %w", err)
}
if time.Now().Unix() > claims.ExpiresAt {return nil, errors.New("token expired")
}
return &claims, nil
}
Redis 缓存策略
我们使用 Redis 缓存有效的 token 和用户信息,设计如下策略:
- 缓存结构
- Key:
token:{tokenID} - Value: 用户基本信息 + 权限列表
-
TTL: 与 JWT 过期时间一致
-
淘汰策略
- 使用 Redis 的 LRU 淘汰策略
-
设置最大内存限制防止 OOM
-
验证流程
- 先验证 JWT 签名和有效期
- 再查询 Redis 确认 token 是否有效
性能优化
基准测试
我们对比了纯 DB 验证和缓存方案的性能:
| 方案 | QPS | 平均延迟 | 99% 延迟 |
|---|---|---|---|
| 纯 DB 验证 | 1,200 | 85ms | 210ms |
| JWT + Redis | 6,800 | 15ms | 35ms |
测试环境:4 核 8G 服务器,Redis 和 DB 在同一内网。
安全防护
为了防止 token 盗用,我们增加了以下防护措施:
- IP 绑定:生成 token 时记录用户 IP,验证时检查是否匹配
- 短期有效性:设置较短的过期时间(如 30 分钟)
- 刷新机制:提供 refresh token 用于获取新 token
避坑指南
时钟漂移问题
JWT 验证依赖系统时间,时钟不同步会导致验证失败。解决方案:
- 使用 NTP 同步服务器时间
- 在验证时增加 5 分钟的时钟偏移容忍
Redis 热点 Key
在集群模式下,所有请求都会访问同一个 Key(如 token:valid)会导致热点问题。解决方案:
- 使用 Hash Tag 将相关 Key 分配到同一节点
- 增加本地缓存减少 Redis 访问
延伸思考
要将该方案扩展为多租户鉴权体系,可以考虑以下改进:
- 在 JWT Claims 中增加租户 ID 字段
- Redis Key 增加租户前缀,如
{tenant}:token:{tokenID} - 为每个租户配置独立的密钥
- 实现租户级别的权限隔离
通过以上优化,我们成功解决了 clawdbot 在高并发场景下的鉴权性能问题,同时保证了系统的安全性。这套方案也可以应用于其他类似的分布式系统中。
正文完
发表至: 未分类
近一天内
