一、项目背景与需求分析
在现代Web应用和微服务架构中,Token(令牌)已成为身份认证和授权的主流方案。无论是JWT、OAuth2.0还是自定义Token,都需要一套完整的管理机制来确保系统的安全性和可用性。然而,许多项目在Token管理方面存在以下痛点:
常见问题:
- Token生成和验证逻辑分散在各个服务中,难以统一维护
- 缺乏统一的Token存储和刷新机制,导致安全漏洞
- 多端登录、单点登录等复杂场景处理不当
- Token过期、续签、注销等生命周期管理不完善
- 缺乏监控和审计能力,无法追踪Token使用情况
核心需求:
- 统一的Token生成、验证、刷新、注销接口
- 支持多种Token类型(JWT、Opaque Token等)
- 可扩展的存储方案(Redis、数据库、内存)
- 多端登录和单点登录支持
- Token生命周期管理(过期、续签、强制失效)
- 安全审计和监控能力
- 高性能和高可用性
二、技术选型与架构设计
2.1 技术栈选择
核心框架:
- Go语言:高性能、并发能力强,适合高并发Token管理场景
- Gin框架:轻量级Web框架,性能优异,中间件生态丰富
- GORM:ORM框架,支持多种数据库,简化数据操作
存储方案:
- Redis:作为Token缓存,支持高并发读写和过期机制
- MySQL/PostgreSQL:持久化存储Token元数据和审计日志
安全组件:
- JWT库:go-jwt库,支持多种签名算法
- 加密库:bcrypt用于密码加密,crypto用于Token签名
其他依赖:
- Viper:配置管理
- Zap:高性能日志库
- Prometheus:监控指标暴露
- Docker:容器化部署
2.2 系统架构设计
┌─────────────────┐ ┌─────────────────┐
│ 客户端 │ │ Token管理服务 │
│ │ │ │
│ ┌───────────┐ │ │ ┌───────────┐ │
│ │ 应用 │ │ │ │ API接口层 │ │
│ └───────────┘ │ │ └───────────┘ │
│ │ │ │ │ │
│ ┌───────────┐ │ │ ┌───────────┐ │
│ │ Token管理 │ │ │ │ 业务逻辑层 │ │
│ │ 客户端SDK │ │ │ └───────────┘ │
│ └───────────┘ │ │ │ │
└────────│─────────┘ │ ┌───────────┐ │
│ │ │ 数据访问层 │ │
└──────────────┼─▶└───────────┘ │
│ │ │
│ ┌───────────┐ │
│ │ 存储层 │ │
│ │ (Redis/DB) │ │
│ └───────────┘ │
└─────────────────┘
架构说明:
- 客户端SDK:封装Token管理逻辑,提供统一的API调用方式
- API接口层:提供RESTful API,处理HTTP请求和响应
- 业务逻辑层:实现Token的生成、验证、刷新、注销等核心逻辑
- 数据访问层:抽象存储接口,支持多种存储方案
- 存储层:Redis用于Token缓存,数据库用于持久化存储
三、核心功能实现
3.1 Token实体定义
// TokenType Token类型
type TokenType string
const (
TokenTypeAccess TokenType = "access" // 访问令牌
TokenTypeRefresh TokenType = "refresh" // 刷新令牌
TokenTypeAPI TokenType = "api" // API令牌
)
// Token 令牌实体
type Token struct {
ID string `json:"id" gorm:"primaryKey;type:varchar(64)"` // Token唯一标识
UserID string `json:"user_id" gorm:"type:varchar(64);index"` // 用户ID
ClientID string `json:"client_id" gorm:"type:varchar(64);index"` // 客户端ID
TokenType TokenType `json:"token_type" gorm:"type:varchar(20)"` // Token类型
TokenValue string `json:"token_value" gorm:"type:text;uniqueIndex"` // Token值
ExpiresAt time.Time `json:"expires_at" gorm:"index"` // 过期时间
RefreshToken string `json:"refresh_token" gorm:"type:varchar(256);index"` // 刷新令牌
RefreshExp time.Time `json:"refresh_exp" gorm:"index"` // 刷新令牌过期时间
Scope string `json:"scope" gorm:"type:varchar(512)"` // 权限范围
Metadata string `json:"metadata" gorm:"type:json"` // 元数据
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` // 创建时间
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` // 更新时间
}
// TokenClaims JWT Claims
type TokenClaims struct {
jwx.StandardClaims
UserID string `json:"user_id"`
ClientID string `json:"client_id"`
Scope string `json:"scope"`
Metadata string `json:"metadata"`
}
3.2 Token生成服务
// TokenService Token服务接口
type TokenService interface {
// GenerateAccessToken 生成访问令牌
GenerateAccessToken(userID, clientID string, scope []string, metadata map[string]interface{}) (*Token, error)
// GenerateRefreshToken 生成刷新令牌
GenerateRefreshToken(userID, clientID string) (*Token, error)
// VerifyToken 验证令牌
VerifyToken(tokenString string) (*TokenClaims, error)
// RefreshToken 刷新令牌
RefreshToken(refreshToken string) (*Token, error)
// RevokeToken 撤销令牌
RevokeToken(tokenID string) error
// RevokeUserTokens 撤销用户所有令牌
RevokeUserTokens(userID string) error
// GetTokenByID 根据ID获取令牌
GetTokenByID(tokenID string) (*Token, error)
// ListTokens 列出令牌
ListTokens(userID, clientID string, page, size int) ([]*Token, int64, error)
}
// tokenServiceImpl Token服务实现
type tokenServiceImpl struct {
config *Config
tokenRepo repository.TokenRepository
redisClient *redis.Client
jwtSigner jwx.Signer
jwtVerifier jwx.Verifier
}
// NewTokenService 创建Token服务实例
func NewTokenService(config *Config, tokenRepo repository.TokenRepository, redisClient *redis.Client) (TokenService, error) {
// 创建JWT签名器
key, err := jwx.NewJWKFromBytes([]byte(config.JWTSecret))
if err != nil {
return nil, fmt.Errorf("failed to create JWK: %v", err)
}
signer, err := jwx.NewSigner(jwx.WithKey(jwx.AlgorithmHS256, key))
if err != nil {
return nil, fmt.Errorf("failed to create signer: %v", err)
}
verifier, err := jwx.NewVerifier(jwx.WithKey(jwx.AlgorithmHS256, key))
if err != nil {
return nil, fmt.Errorf("failed to create verifier: %v", err)
}
return &tokenServiceImpl{
config: config,
tokenRepo: tokenRepo,
redisClient: redisClient,
jwtSigner: signer,
jwtVerifier: verifier,
}, nil
}
// GenerateAccessToken 生成访问令牌
func (s *tokenServiceImpl) GenerateAccessToken(userID, clientID string, scope []string, metadata map[string]interface{}) (*Token, error) {
// 生成Token ID
tokenID := uuid.New().String()
// 设置过期时间
expiresAt := time.Now().Add(s.config.AccessTokenExpiry)
// 构建Claims
claims := &TokenClaims{
StandardClaims: jwx.StandardClaims{
ID: tokenID,
Subject: userID,
IssuedAt: time.Now().Unix(),
ExpiresAt: expiresAt.Unix(),
Issuer: s.config.Issuer,
},
UserID: userID,
ClientID: clientID,
Scope: strings.Join(scope, " "),
}
// 序列化元数据
if metadata != nil {
metadataBytes, err := json.Marshal(metadata)
if err != nil {
return nil, fmt.Errorf("failed to marshal metadata: %v", err)
}
claims.Metadata = string(metadataBytes)
}
// 生成JWT Token
tokenBytes, err := jwx.Sign(claims, s.jwtSigner)
if err != nil {
return nil, fmt.Errorf("failed to sign token: %v", err)
}
tokenString := string(tokenBytes)
// 生成刷新令牌
refreshToken, err := s.GenerateRefreshToken(userID, clientID)
if err != nil {
return nil, err
}
// 创建Token实体
token := &Token{
ID: tokenID,
UserID: userID,
ClientID: clientID,
TokenType: TokenTypeAccess,
TokenValue: tokenString,
ExpiresAt: expiresAt,
RefreshToken: refreshToken.TokenValue,
RefreshExp: refreshToken.ExpiresAt,
Scope: strings.Join(scope, " "),
Metadata: claims.Metadata,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// 保存到数据库
if err := s.tokenRepo.Create(token); err != nil {
return nil, fmt.Errorf("failed to save token to database: %v", err)
}
// 保存到Redis缓存
if err := s.saveTokenToRedis(token); err != nil {
log.Warnf("failed to save token to redis: %v", err)
}
return token, nil
}
// GenerateRefreshToken 生成刷新令牌
func (s *tokenServiceImpl) GenerateRefreshToken(userID, clientID string) (*Token, error) {
tokenID := uuid.New().String()
expiresAt := time.Now().Add(s.config.RefreshTokenExpiry)
// 刷新令牌使用不透明Token,不包含Claims
tokenValue := generateOpaqueToken()
token := &Token{
ID: tokenID,
UserID: userID,
ClientID: clientID,
TokenType: TokenTypeRefresh,
TokenValue: tokenValue,
ExpiresAt: expiresAt,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := s.tokenRepo.Create(token); err != nil {
return nil, fmt.Errorf("failed to save refresh token: %v", err)
}
if err := s.saveTokenToRedis(token); err != nil {
log.Warnf("failed to save refresh token to redis: %v", err)
}
return token, nil
}
// generateOpaqueToken 生成不透明Token
func generateOpaqueToken() string {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
panic(err)
}
return base64.URLEncoding.EncodeToString(b)
}
// saveTokenToRedis 保存Token到Redis
func (s *tokenServiceImpl) saveTokenToRedis(token *Token) error {
ctx := context.Background()
// 序列化Token
tokenBytes, err := json.Marshal(token)
if err != nil {
return fmt.Errorf("failed to marshal token: %v", err)
}
// 计算过期时间
expiry := time.Until(token.ExpiresAt)
if expiry < 0 {
return fmt.Errorf("token already expired")
}
// 保存到Redis
if err := s.redisClient.Set(ctx, buildTokenKey(token.TokenValue), tokenBytes, expiry).Err(); err != nil {
return fmt.Errorf("failed to set token to redis: %v", err)
}
return nil
}
// buildTokenKey 构建Redis Key
func buildTokenKey(tokenValue string) string {
return fmt.Sprintf("token:%s", tokenValue)
}
3.3 Token验证服务
// VerifyToken 验证令牌
func (s *tokenServiceImpl) VerifyToken(tokenString string) (*TokenClaims, error) {
// 首先从Redis缓存中查找
if claims, err := s.verifyTokenFromRedis(tokenString); err == nil {
return claims, nil
}
// 如果Redis中没有,从数据库查找并验证
token, err := s.tokenRepo.FindByTokenValue(tokenString)
if err != nil {
return nil, fmt.Errorf("token not found: %v", err)
}
// 检查Token是否过期
if time.Now().After(token.ExpiresAt) {
return nil, fmt.Errorf("token expired")
}
// 验证JWT签名
claims := &TokenClaims{}
if err := jwx.Verify([]byte(tokenString), s.jwtVerifier, claims); err != nil {
return nil, fmt.Errorf("invalid token signature: %v", err)
}
// 验证Token ID是否匹配
if claims.ID != token.ID {
return nil, fmt.Errorf("token id mismatch")
}
// 保存到Redis缓存
if err := s.saveTokenToRedis(token); err != nil {
log.Warnf("failed to save token to redis: %v", err)
}
return claims, nil
}
// verifyTokenFromRedis 从Redis验证令牌
func (s *tokenServiceImpl) verifyTokenFromRedis(tokenString string) (*TokenClaims, error) {
ctx := context.Background()
// 从Redis获取Token
tokenBytes, err := s.redisClient.Get(ctx, buildTokenKey(tokenString)).Bytes()
if err != nil {
if err == redis.Nil {
return nil, fmt.Errorf("token not found in redis")
}
return nil, fmt.Errorf("failed to get token from redis: %v", err)
}
// 反序列化Token
var token Token
if err := json.Unmarshal(tokenBytes, &token); err != nil {
return nil, fmt.Errorf("failed to unmarshal token: %v", err)
}
// 检查Token是否过期
if time.Now().After(token.ExpiresAt) {
// 从Redis删除过期Token
s.redisClient.Del(ctx, buildTokenKey(tokenString))
return nil, fmt.Errorf("token expired")
}
// 解析Claims
claims := &TokenClaims{}
if err := jwx.ParseClaims([]byte(tokenString), claims); err != nil {
return nil, fmt.Errorf("failed to parse claims: %v", err)
}
return claims, nil
}
3.4 Token刷新服务
// RefreshToken 刷新令牌
func (s *tokenServiceImpl) RefreshToken(refreshToken string) (*Token, error) {
// 验证刷新令牌
token, err := s.tokenRepo.FindByTokenValue(refreshToken)
if err != nil {
return nil, fmt.Errorf("refresh token not found: %v", err)
}
// 检查刷新令牌是否过期
if time.Now().After(token.ExpiresAt) {
return nil, fmt.Errorf("refresh token expired")
}
// 检查刷新令牌类型
if token.TokenType != TokenTypeRefresh {
return nil, fmt.Errorf("invalid refresh token type")
}
// 撤销旧的访问令牌
if err := s.revokeUserAccessTokens(token.UserID, token.ClientID); err != nil {
return nil, fmt.Errorf("failed to revoke old tokens: %v", err)
}
// 解析原访问令牌的Scope
var scope []string
if token.Scope != "" {
scope = strings.Split(token.Scope, " ")
}
// 解析元数据
var metadata map[string]interface{}
if token.Metadata != "" {
if err := json.Unmarshal([]byte(token.Metadata), &metadata); err != nil {
return nil, fmt.Errorf("failed to unmarshal metadata: %v", err)
}
}
// 生成新的访问令牌
newAccessToken, err := s.GenerateAccessToken(token.UserID, token.ClientID, scope, metadata)
if err != nil {
return nil, fmt.Errorf("failed to generate new access token: %v", err)
}
// 撤销旧的刷新令牌
if err := s.RevokeToken(token.ID); err != nil {
return nil, fmt.Errorf("failed to revoke old refresh token: %v", err)
}
return newAccessToken, nil
}
// revokeUserAccessTokens 撤销用户的所有访问令牌
func (s *tokenServiceImpl) revokeUserAccessTokens(userID, clientID string) error {
tokens, err := s.tokenRepo.FindByUserIDAndClientID(userID, clientID, TokenTypeAccess)
if err != nil {
return fmt.Errorf("failed to find user tokens: %v", err)
}
for _, token := range tokens {
if err := s.RevokeToken(token.ID); err != nil {
log.Warnf("failed to revoke token %s: %v", token.ID, err)
}
}
return nil
}
3.5 Token撤销服务
// RevokeToken 撤销令牌
func (s *tokenServiceImpl) RevokeToken(tokenID string) error {
// 从数据库查找Token
token, err := s.tokenRepo.FindByID(tokenID)
if err != nil {
return fmt.Errorf("token not found: %v", err)
}
// 从数据库删除
if err := s.tokenRepo.Delete(tokenID); err != nil {
return fmt.Errorf("failed to delete token from database: %v", err)
}
// 从Redis删除
ctx := context.Background()
if err := s.redisClient.Del(ctx, buildTokenKey(token.TokenValue)).Err(); err != nil {
log.Warnf("failed to delete token from redis: %v", err)
}
return nil
}
// RevokeUserTokens 撤销用户所有令牌
func (s *tokenServiceImpl) RevokeUserTokens(userID string) error {
// 查找用户的所有Token
tokens, err := s.tokenRepo.FindByUserID(userID)
if err != nil {
return fmt.Errorf("failed to find user tokens: %v", err)
}
// 批量删除
for _, token := range tokens {
if err := s.RevokeToken(token.ID); err != nil {
log.Warnf("failed to revoke token %s: %v", token.ID, err)
}
}
return nil
}
四、数据访问层实现
4.1 存储接口定义
// TokenRepository Token存储接口
type TokenRepository interface {
// Create 创建Token
Create(token *Token) error
// FindByID 根据ID查找Token
FindByID(id string) (*Token, error)
// FindByTokenValue 根据Token值查找
FindByTokenValue(tokenValue string) (*Token, error)
// FindByUserID 根据用户ID查找
FindByUserID(userID string) ([]*Token, error)
// FindByUserIDAndClientID 根据用户ID和客户端ID查找
FindByUserIDAndClientID(userID, clientID string, tokenType TokenType) ([]*Token, error)
// Delete 删除Token
Delete(id string) error
// DeleteExpired 删除过期Token
DeleteExpired() error
// List 列出Token
List(page, size int) ([]*Token, int64, error)
}
// mysqlTokenRepository MySQL实现
type mysqlTokenRepository struct {
db *gorm.DB
}
// NewMySQLTokenRepository 创建MySQL存储实例
func NewMySQLTokenRepository(db *gorm.DB) TokenRepository {
return &mysqlTokenRepository{db: db}
}
// Create 创建Token
func (r *mysqlTokenRepository) Create(token *Token) error {
return r.db.Create(token).Error
}
// FindByID 根据ID查找Token
func (r *mysqlTokenRepository) FindByID(id string) (*Token, error) {
var token Token
if err := r.db.Where("id = ?", id).First(&token).Error; err != nil {
return nil, err
}
return &token, nil
}
// FindByTokenValue 根据Token值查找
func (r *mysqlTokenRepository) FindByTokenValue(tokenValue string) (*Token, error) {
var token Token
if err := r.db.Where("token_value = ?", tokenValue).First(&token).Error; err != nil {
return nil, err
}
return &token, nil
}
// FindByUserID 根据用户ID查找
func (r *mysqlTokenRepository) FindByUserID(userID string) ([]*Token, error) {
var tokens []*Token
if err := r.db.Where("user_id = ?", userID).Find(&tokens).Error; err != nil {
return nil, err
}
return tokens, nil
}
// FindByUserIDAndClientID 根据用户ID和客户端ID查找
func (r *mysqlTokenRepository) FindByUserIDAndClientID(userID, clientID string, tokenType TokenType) ([]*Token, error) {
var tokens []*Token
query := r.db.Where("user_id = ? AND client_id = ?", userID, clientID)
if tokenType != "" {
query = query.Where("token_type = ?", tokenType)
}
if err := query.Find(&tokens).Error; err != nil {
return nil, err
}
return tokens, nil
}
// Delete 删除Token
func (r *mysqlTokenRepository) Delete(id string) error {
return r.db.Where("id = ?", id).Delete(&Token{}).Error
}
// DeleteExpired 删除过期Token
func (r *mysqlTokenRepository) DeleteExpired() error {
return r.db.Where("expires_at < ?", time.Now()).Delete(&Token{}).Error
}
// List 列出Token
func (r *mysqlTokenRepository) List(page, size int) ([]*Token, int64, error) {
var tokens []*Token
var total int64
offset := (page - 1) * size
if err := r.db.Model(&Token{}).Count(&total).Offset(offset).Limit(size).Find(&tokens).Error; err != nil {
return nil, 0, err
}
return tokens, total, nil
}
五、API接口层实现
5.1 HTTP接口定义
// TokenHandler Token HTTP处理器
type TokenHandler struct {
tokenService service.TokenService
}
// NewTokenHandler 创建Token处理器
func NewTokenHandler(tokenService service.TokenService) *TokenHandler {
return &TokenHandler{tokenService: tokenService}
}
// GenerateTokenRequest 生成Token请求
type GenerateTokenRequest struct {
UserID string `json:"user_id" binding:"required"`
ClientID string `json:"client_id" binding:"required"`
Scope []string `json:"scope"`
Metadata map[string]interface{} `json:"metadata"`
}
// GenerateTokenResponse 生成Token响应
type GenerateTokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
Scope string `json:"scope"`
}
// RefreshTokenRequest 刷新Token请求
type RefreshTokenRequest struct {
RefreshToken string `json:"refresh_token" binding:"required"`
}
// VerifyTokenRequest 验证Token请求
type VerifyTokenRequest struct {
Token string `json:"token" binding:"required"`
}
// VerifyTokenResponse 验证Token响应
type VerifyTokenResponse struct {
Valid bool `json:"valid"`
Claims *TokenClaims `json:"claims,omitempty"`
Message string `json:"message,omitempty"`
}
// GenerateToken 生成Token
func (h *TokenHandler) GenerateToken(c *gin.Context) {
var req GenerateTokenRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
token, err := h.tokenService.GenerateAccessToken(req.UserID, req.ClientID, req.Scope, req.Metadata)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
resp := GenerateTokenResponse{
AccessToken: token.TokenValue,
TokenType: "Bearer",
ExpiresIn: int64(time.Until(token.ExpiresAt).Seconds()),
RefreshToken: token.RefreshToken,
Scope: token.Scope,
}
c.JSON(http.StatusOK, resp)
}
// RefreshToken 刷新Token
func (h *TokenHandler) RefreshToken(c *gin.Context) {
var req RefreshTokenRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
token, err := h.tokenService.RefreshToken(req.RefreshToken)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
resp := GenerateTokenResponse{
AccessToken: token.TokenValue,
TokenType: "Bearer",
ExpiresIn: int64(time.Until(token.ExpiresAt).Seconds()),
RefreshToken: token.RefreshToken,
Scope: token.Scope,
}
c.JSON(http.StatusOK, resp)
}
// VerifyToken 验证Token
func (h *TokenHandler) VerifyToken(c *gin.Context) {
var req VerifyTokenRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
claims, err := h.tokenService.VerifyToken(req.Token)
if err != nil {
c.JSON(http.StatusOK, VerifyTokenResponse{
Valid: false,
Message: err.Error(),
})
return
}
c.JSON(http.StatusOK, VerifyTokenResponse{
Valid: true,
Claims: claims,
})
}
// RevokeToken 撤销Token
func (h *TokenHandler) RevokeToken(c *gin.Context) {
tokenID := c.Param("id")
if tokenID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "token id is required"})
return
}
if err := h.tokenService.RevokeToken(tokenID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "token revoked successfully"})
}
// RevokeUserTokens 撤销用户所有Token
func (h *TokenHandler) RevokeUserTokens(c *gin.Context) {
userID := c.Param("user_id")
if userID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "user id is required"})
return
}
if err := h.tokenService.RevokeUserTokens(userID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "user tokens revoked successfully"})
}
5.2 路由注册
// SetupRouter 设置路由
func SetupRouter(tokenService service.TokenService) *gin.Engine {
r := gin.Default()
// 添加中间件
r.Use(gin.Recovery())
r.Use(middleware.CORS())
r.Use(middleware.Logger())
// 创建Token处理器
tokenHandler := NewTokenHandler(tokenService)
// Token相关路由
tokenGroup := r.Group("/api/v1/tokens")
{
tokenGroup.POST("/generate", tokenHandler.GenerateToken)
tokenGroup.POST("/refresh", tokenHandler.RefreshToken)
tokenGroup.POST("/verify", tokenHandler.VerifyToken)
tokenGroup.DELETE("/:id", tokenHandler.RevokeToken)
tokenGroup.DELETE("/user/:user_id", tokenHandler.RevokeUserTokens)
}
return r
}
六、中间件实现
6.1 Token认证中间件
// AuthMiddleware Token认证中间件
func AuthMiddleware(tokenService service.TokenService) gin.HandlerFunc {
return func(c *gin.Context) {
// 从Header中获取Token
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "authorization header is required"})
c.Abort()
return
}
// 解析Bearer Token
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid authorization header format"})
c.Abort()
return
}
tokenString := parts[1]
// 验证Token
claims, err := tokenService.VerifyToken(tokenString)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token: " + err.Error()})
c.Abort()
return
}
// 将用户信息保存到Context
c.Set("user_id", claims.UserID)
c.Set("client_id", claims.ClientID)
c.Set("scope", claims.Scope)
c.Next()
}
}
6.2 权限验证中间件
// ScopeRequired 权限验证中间件
func ScopeRequired(requiredScope string) gin.HandlerFunc {
return func(c *gin.Context) {
// 获取用户权限
scope, exists := c.Get("scope")
if !exists {
c.JSON(http.StatusForbidden, gin.H{"error": "scope not found in context"})
c.Abort()
return
}
scopeStr, ok := scope.(string)
if !ok {
c.JSON(http.StatusForbidden, gin.H{"error": "invalid scope type"})
c.Abort()
return
}
// 检查权限
scopes := strings.Split(scopeStr, " ")
hasScope := false
for _, s := range scopes {
if s == requiredScope {
hasScope = true
break
}
}
if !hasScope {
c.JSON(http.StatusForbidden, gin.H{"error": "insufficient scope"})
c.Abort()
return
}
c.Next()
}
}
七、配置管理
7.1 配置文件
# config.yaml
server:
port: 8080
mode: "release"
database:
host: "localhost"
port: 3306
user: "root"
password: "password"
name: "token_manager"
max_idle_conns: 10
max_open_conns: 100
conn_max_lifetime: "1h"
redis:
addr: "localhost:6379"
password: ""
db: 0
pool_size: 100
min_idle_conns: 10
jwt:
secret: "your-secret-key"
issuer: "token-manager"
access_token_expiry: "2h"
refresh_token_expiry: "720h" # 30天
log:
level: "info"
output: "stdout"
7.2 配置结构体
// Config 配置结构体
type Config struct {
Server ServerConfig `mapstructure:"server"`
Database DatabaseConfig `mapstructure:"database"`
Redis RedisConfig `mapstructure:"redis"`
JWT JWTConfig `mapstructure:"jwt"`
Log LogConfig `mapstructure:"log"`
}
// ServerConfig 服务器配置
type ServerConfig struct {
Port int `mapstructure:"port"`
Mode string `mapstructure:"mode"`
}
// DatabaseConfig 数据库配置
type DatabaseConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
Name string `mapstructure:"name"`
MaxIdleConns int `mapstructure:"max_idle_conns"`
MaxOpenConns int `mapstructure:"max_open_conns"`
ConnMaxLifetime string `mapstructure:"conn_max_lifetime"`
}
// RedisConfig Redis配置
type RedisConfig struct {
Addr string `mapstructure:"addr"`
Password string `mapstructure:"password"`
DB int `mapstructure:"db"`
PoolSize int `mapstructure:"pool_size"`
MinIdleConns int `mapstructure:"min_idle_conns"`
}
// JWTConfig JWT配置
type JWTConfig struct {
Secret string `mapstructure:"secret"`
Issuer string `mapstructure:"issuer"`
AccessTokenExpiry string `mapstructure:"access_token_expiry"`
RefreshTokenExpiry string `mapstructure:"refresh_token_expiry"`
}
// LogConfig 日志配置
type LogConfig struct {
Level string `mapstructure:"level"`
Output string `mapstructure:"output"`
}
// LoadConfig 加载配置
func LoadConfig(path string) (*Config, error) {
viper.SetConfigFile(path)
viper.SetConfigType("yaml")
if err := viper.ReadInConfig(); err != nil {
return nil, fmt.Errorf("failed to read config file: %v", err)
}
var config Config
if err := viper.Unmarshal(&config); err != nil {
return nil, fmt.Errorf("failed to unmarshal config: %v", err)
}
return &config, nil
}
八、应用启动
8.1 主函数
// main.go
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
"github.com/redis/go-redis/v9"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"token-manager/config"
"token-manager/repository"
"token-manager/service"
)
func main() {
// 加载配置
cfg, err := config.LoadConfig("config.yaml")
if err != nil {
log.Fatalf("failed to load config: %v", err)
}
// 初始化数据库
db, err := initDatabase(cfg.Database)
if err != nil {
log.Fatalf("failed to init database: %v", err)
}
// 初始化Redis
redisClient := initRedis(cfg.Redis)
// 创建Token存储
tokenRepo := repository.NewMySQLTokenRepository(db)
// 创建Token服务
tokenService, err := service.NewTokenService(cfg, tokenRepo, redisClient)
if err != nil {
log.Fatalf("failed to create token service: %v", err)
}
// 设置Gin模式
if cfg.Server.Mode == "release" {
gin.SetMode(gin.ReleaseMode)
}
// 创建路由
r := SetupRouter(tokenService)
// 启动服务器
srv := &http.Server{
Addr: fmt.Sprintf(":%d", cfg.Server.Port),
Handler: r,
}
// 优雅关闭
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
}()
log.Printf("server started on port %d", cfg.Server.Port)
// 等待中断信号
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatalf("server forced to shutdown: %v", err)
}
log.Println("server exited")
}
// initDatabase 初始化数据库
func initDatabase(cfg config.DatabaseConfig) (*gorm.DB, error) {
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.Name)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
return nil, err
}
sqlDB, err := db.DB()
if err != nil {
return nil, err
}
// 设置连接池
sqlDB.SetMaxIdleConns(cfg.MaxIdleConns)
sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)
maxLifetime, err := time.ParseDuration(cfg.ConnMaxLifetime)
if err != nil {
return nil, err
}
sqlDB.SetConnMaxLifetime(maxLifetime)
// 自动迁移
if err := db.AutoMigrate(&service.Token{}); err != nil {
return nil, err
}
return db, nil
}
// initRedis 初始化Redis
func initRedis(cfg config.RedisConfig) *redis.Client {
return redis.NewClient(&redis.Options{
Addr: cfg.Addr,
Password: cfg.Password,
DB: cfg.DB,
PoolSize: cfg.PoolSize,
})
}
九、完整使用示例
9.1 生成Token
# 生成访问令牌
curl -X POST http://localhost:8080/api/v1/tokens/generate \
-H "Content-Type: application/json" \
-d '{
"user_id": "user123",
"client_id": "web-app",
"scope": ["read", "write"],
"metadata": {"ip": "192.168.1.1", "device": "web"}
}'
# 响应示例
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 7200,
"refresh_token": "mF_9.B5f-4.1JqM",
"scope": "read write"
}
9.2 验证Token
# 验证令牌
curl -X POST http://localhost:8080/api/v1/tokens/verify \
-H "Content-Type: application/json" \
-d '{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}'
# 响应示例
{
"valid": true,
"claims": {
"user_id": "user123",
"client_id": "web-app",
"scope": "read write",
"metadata": "{\"ip\":\"192.168.1.1\",\"device\":\"web\"}"
}
}
9.3 刷新Token
# 刷新令牌
curl -X POST http://localhost:8080/api/v1/tokens/refresh \
-H "Content-Type: application/json" \
-d '{
"refresh_token": "mF_9.B5f-4.1JqM"
}'
# 响应示例
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 7200,
"refresh_token": "new-refresh-token-value",
"scope": "read write"
}
9.4 撤销Token
# 撤销单个令牌
curl -X DELETE http://localhost:8080/api/v1/tokens/token_id_123
# 撤销用户所有令牌
curl -X DELETE http://localhost:8080/api/v1/tokens/user/user123
9.5 使用中间件保护API
// 在需要保护的API上使用认证中间件
r.GET("/api/v1/protected", AuthMiddleware(tokenService), func(c *gin.Context) {
userID := c.MustGet("user_id").(string)
c.JSON(http.StatusOK, gin.H{"message": "Hello " + userID})
})
// 使用权限验证中间件
r.GET("/api/v1/admin", AuthMiddleware(tokenService), ScopeRequired("admin"), func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Welcome admin"})
})
十、设计说明与最佳实践
10.1 安全性设计
Token安全:
- 使用HS256算法签名JWT,确保Token不被篡改
- 设置合理的过期时间(访问令牌2小时,刷新令牌30天)
- 刷新令牌使用不透明Token,不包含敏感信息
- 支持Token强制撤销,防止Token被盗用
存储安全:
- Token在Redis中设置自动过期,避免内存泄漏
- 数据库存储Token元数据,支持审计和查询
- 敏感配置(如JWT密钥)通过环境变量或配置中心管理
传输安全:
- 建议使用HTTPS协议传输Token
- 在HTTP Header中使用Bearer Token,避免URL参数泄露
10.2 性能优化
缓存策略:
- 使用Redis缓存热点Token,减少数据库查询
- 设置合理的缓存过期时间,与Token过期时间一致
- 使用Pipeline批量操作,减少网络往返次数
连接池:
- 数据库连接池大小根据并发量调整
- Redis连接池大小建议设置为CPU核心数的2-3倍
- 设置连接最大空闲时间,避免连接泄漏
异步处理:
- Token生成和验证同步处理,保证实时性
- Token撤销和清理使用异步任务,避免阻塞主流程
10.3 可扩展性
存储扩展:
- 支持多种数据库(MySQL、PostgreSQL、MongoDB)
- 支持多种缓存(Redis、Memcached)
- 通过接口抽象,易于扩展新的存储方案
本文完结
若内容若侵犯到您的权益,请发送邮件至:platform_service@jienda.com我们将第一时间处理!
所有资源仅限于参考和学习,版权归JienDa作者所有,更多请访问JienDa首页。





