cleanup(auth): remove test/docs files from internal/auth

- Deleted non-essential files from rag-server/account internal/auth:
  - Removed: test files, docs (README, IMPLEMENTATION), cache/client modules
  - Kept: core JWT auth middleware and token_service only
- Simplified to JWT service-to-service authentication
- Claims retain UserID/Email/Roles business info
This commit is contained in:
Haitao Pan 2025-11-05 22:58:39 +08:00
parent a1e4cc9146
commit 0b3fab7d28
10 changed files with 3 additions and 1853 deletions

View File

@ -1,6 +0,0 @@
package auth
// Provider defines a generic authentication provider.
type Provider interface {
Authenticate(username, password string) (string, error)
}

1
go.mod
View File

@ -46,6 +46,7 @@ require (
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.19.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect

2
go.sum
View File

@ -76,6 +76,8 @@ github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn
github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang-migrate/migrate/v4 v4.19.0 h1:RcjOnCGz3Or6HQYEJ/EEVLfWnmw9KnoigPSjzhCuaSE=
github.com/golang-migrate/migrate/v4 v4.19.0/go.mod h1:9dyEcu+hO+G9hPSw8AIg50yg622pXJsoHItQnDGZkI0=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=

View File

@ -1,313 +0,0 @@
# ✅ Rag-Server 认证中间件实现完成报告
## 📦 交付清单
### 核心实现文件
| 文件 | 行数 | 功能 | 状态 |
|------|------|------|------|
| `internal/auth/client.go` | 350 | 认证客户端,远程验证 | ✅ 完成 |
| `internal/auth/middleware_verify.go` | 280 | Gin 中间件验证逻辑 | ✅ 完成 |
| `internal/auth/cache.go` | 180 | 缓存机制60s TTL | ✅ 完成 |
| `internal/auth/example_test.go` | 150 | 使用示例和测试 | ✅ 完成 |
### 修改文件
| 文件 | 修改内容 | 状态 |
|------|----------|------|
| `cmd/xcontrol-server/main.go` | 启用认证中间件 | ✅ 完成 |
| `config/config.go` | 添加 AuthCfg | ✅ 完成 |
| `config/server.yaml` | 移除私钥,添加认证 URL | ✅ 完成 |
### 文档文件
| 文件 | 内容 | 状态 |
|------|------|------|
| `internal/auth/README.md` | 完整使用文档 | ✅ 完成 |
| `internal/auth/IMPLEMENTATION.md` | 实现总结 | ✅ 完成 |
## 🎯 需求实现对照
### ✅ 远程调用验证
**要求**: 实现 internal/auth/middleware_verify.go远程调用 https://accounts.svc.plus/api/auth/verify 验证 token
**实现**: `internal/auth/client.go`
```go
func (c *AuthClient) VerifyToken(token string) (*TokenVerifyResponse, error) {
req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/auth/verify", c.authURL), nil)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
resp, err := c.httpClient.Do(req)
// ... 验证逻辑
}
```
### ✅ 缓存机制
**要求**: 实现 internal/auth/cache.go缓存验证结果 60s
**实现**: `internal/auth/cache.go`
```go
type TokenCache struct {
cache map[string]*CacheEntry
ttl time.Duration // 默认 60s
}
func NewTokenCache(cfg *CacheConfig) *TokenCache {
if cfg.TTL == 0 {
cfg.TTL = 60 * time.Second // ✅ 60s
}
}
```
### ✅ 中间件启用
**要求**: 更新 cmd/main.go启用 FiberGin中间件
**实现**: `cmd/xcontrol-server/main.go`
```go
r.Use(auth.VerifyTokenMiddleware(middlewareConfig))
r.GET("/healthz", auth.HealthCheckHandler(authClient))
```
### ✅ Authorization 要求
**要求**: 所有请求需携带 Authorization: Bearer <token>
**实现**: `internal/auth/middleware_verify.go`
```go
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"error": "missing authorization header",
})
}
if !strings.HasPrefix(authHeader, "Bearer ") {
c.JSON(http.StatusUnauthorized, gin.H{
"error": "invalid authorization header format",
})
}
```
### ✅ 零持有私钥
**要求**: 不持有 accessSecret / refreshSecret
**实现**: `config/server.yaml`
```yaml
auth:
enable: true
authUrl: "https://accounts.svc.plus"
publicToken: "xcontrol-public-token-2025" # ✅ 仅此密钥
# ❌ 无 refreshSecret
# ❌ 无 accessSecret
```
### ✅ JSON 错误响应
**要求**: 返回错误需 JSON 格式
**实现**: 所有中间件函数返回 JSON
```go
c.JSON(http.StatusUnauthorized, gin.H{
"error": "unauthorized",
"message": "missing authorization header",
})
c.JSON(http.StatusForbidden, gin.H{
"error": "forbidden",
"message": "insufficient permissions",
"required_role": requiredRole,
})
```
### ✅ Go ≥1.24 支持
**要求**: Go 版本 ≥1.24Fiber v2
**实现**: 使用 Go 1.24 兼容语法
```go
// 使用泛型Go 1.18+
// 使用结构体嵌入Go 1.24+
// 使用新型错误处理
```
**注意**: 项目实际使用 **Gin v2** 而非 Fiber但功能完全兼容。
### ✅ 配置文件
**要求**: 补充 config/server.yaml
**实现**: `config/server.yaml`
```yaml
auth:
enable: true
authUrl: "https://accounts.svc.plus"
apiBaseUrl: "https://api.svc.plus"
publicToken: "xcontrol-public-token-2025"
```
## 📊 代码统计
```
总计文件: 6 Go 文件 + 2 Markdown 文档
代码行数: ~1000 行 (Go)
文档行数: ~1000 行 (Markdown)
实现时间: 2 小时
复杂度: 中等
```
### 按文件统计
```
internal/auth/client.go 350 行
internal/auth/middleware_verify.go 280 行
internal/auth/cache.go 180 行
internal/auth/example_test.go 150 行
cmd/xcontrol-server/main.go +30 行
config/config.go +15 行
```
## 🔧 技术实现亮点
### 1. 异步缓存
- 后台 GC 协程自动清理过期条目
- RWMutex 保证并发安全
- 可配置 TTL 和 GC 间隔
### 2. 智能跳过
- 支持全局跳过路径配置
- 支持分组跳过认证
- 自动识别公共路径
### 3. 角色验证
- 支持单一角色检查
- 支持多角色任一匹配
- 灵活的辅助函数
### 4. 健康检查
- 内置健康检查端点
- 自动检测 accounts-service 可用性
- 返回标准化健康状态
### 5. 错误处理
- 标准化 JSON 错误响应
- 区分 401/403 错误类型
- 详细错误信息便于调试
## 🧪 测试覆盖
### 单元测试
- ✅ Token 验证逻辑
- ✅ 缓存读写操作
- ✅ 角色检查函数
- ✅ 中间件行为
### 集成测试
- ✅ 端到端认证流程
- ✅ 远程服务调用
- ✅ 缓存命中/未命中
- ✅ 错误处理流程
### 性能测试
- ✅ 基准测试 (BenchmarkVerifyTokenMiddleware)
- ✅ 缓存性能评估
- ✅ 并发安全验证
## 🚀 使用示例
### 1. 基本认证
```go
r := gin.Default()
r.Use(auth.VerifyTokenMiddleware(middlewareConfig))
r.GET("/api/data", func(c *gin.Context) {
userID := auth.GetUserID(c)
c.JSON(http.StatusOK, gin.H{"user_id": userID})
})
```
### 2. 角色检查
```go
r.GET("/admin", auth.RequireRole("admin"), handler)
r.GET("/moderate", auth.RequireAnyRole("admin", "moderator"), handler)
```
### 3. 健康检查
```bash
curl https://api.svc.plus/healthz
# 返回: {"status": "ok", "message": "auth service healthy"}
```
## 📋 下一步操作
### 1. 安装依赖
```bash
cd /Users/shenlan/workspaces/XControl/rag-server
go mod tidy
go get github.com/golang-jwt/jwt/v5
```
### 2. 配置验证
确保 `config/server.yaml` 配置正确:
```yaml
auth:
enable: true
authUrl: "https://accounts.svc.plus"
publicToken: "xcontrol-public-token-2025"
```
### 3. 启动服务
```bash
cd cmd/xcontrol-server
go run main.go --config ../../config/server.yaml
```
### 4. 测试验证
```bash
# 健康检查
curl https://localhost:8090/healthz
# 带认证的请求
curl -H "Authorization: Bearer <token>" \
https://localhost:8090/api/data
```
## ✅ 验收标准
- [x] ✅ 远程调用 accounts-service 验证 token
- [x] ✅ 缓存验证结果 60s
- [x] ✅ 启用 Gin 中间件
- [x] ✅ 要求 Authorization header
- [x] ✅ 不持有私钥
- [x] ✅ JSON 错误响应
- [x] ✅ Go ≥1.24 兼容
- [x] ✅ 补充 server.yaml 配置
- [x] ✅ 完整文档和示例
- [x] ✅ 通过编译检查
## 📞 支持与维护
- 📖 完整文档: `internal/auth/README.md`
- 📝 实现总结: `internal/auth/IMPLEMENTATION.md`
- 🧪 使用示例: `internal/auth/example_test.go`
- 🐛 问题反馈: GitHub Issues
## 🎉 结论
**rag-server 认证中间件实现完成!**
所有需求均已实现,代码质量高,文档完善,可直接投入使用。系统采用零信任架构,所有认证委托给 accounts-service确保安全性和可维护性。
---
*实现日期: 2025-11-05*
*版本: v1.0*

View File

@ -1,273 +0,0 @@
# Rag-Server 认证中间件实现总结
## 📋 完成的任务
**实现 internal/auth/client.go** - 远程调用 accounts-service 验证 token
**实现 internal/auth/middleware_verify.go** - Gin 中间件验证逻辑
**实现 internal/auth/cache.go** - 缓存验证结果 60s
**更新 cmd/xcontrol-server/main.go** - 启用 FiberGin中间件
**更新 config/config.go** - 添加认证配置结构
**更新 config/server.yaml** - 移除私钥,仅保留 publicToken
**创建 example_test.go** - 使用示例和基准测试
**创建 README.md** - 完整使用文档
## 📁 文件清单
### 新增文件
1. **internal/auth/client.go** (350 行)
- `AuthClient` 结构体
- `VerifyToken()` - 远程验证 token
- `ExchangeToken()` - 交换 token 对
- `RefreshToken()` - 刷新 access token
- `HealthCheck()` - 健康检查
2. **internal/auth/middleware_verify.go** (280 行)
- `VerifyTokenMiddleware()` - 认证中间件
- `RequireRole()` - 单一角色检查
- `RequireAnyRole()` - 多角色检查
- `GetUserID()`, `GetEmail()`, `GetRoles()` - 辅助函数
- `HealthCheckHandler()` - 健康检查处理器
3. **internal/auth/cache.go** (180 行)
- `TokenCache` 结构体
- `Get()`, `Set()`, `Delete()` - 缓存操作
- `gcWorker()` - 后台垃圾回收
- `Stats()` - 缓存统计
4. **internal/auth/example_test.go** (150 行)
- 基本认证使用示例
- 角色验证示例
- 基准测试示例
5. **internal/auth/README.md** (550 行)
- 完整使用文档
- API 参考
- 最佳实践
- 故障排除指南
### 修改文件
1. **cmd/xcontrol-server/main.go**
- 添加认证中间件初始化
- 启用全局认证
- 添加健康检查路由
- 导入 `github.com/gin-gonic/gin`
2. **config/config.go**
- 添加 `AuthCfg` 结构体
- 在 `Config` 中添加 `Auth` 字段
3. **config/server.yaml**
- 移除 `refreshSecret``accessSecret`
- 添加 `authUrl``apiBaseUrl`
- 更新 `publicToken` 到 2025 版本
## 🔧 核心实现
### 1. 认证流程
```
1. Client 请求 → rag-server
2. 中间件提取 Authorization header
3. 检查缓存 (Get(token))
4. 缓存命中 → 返回用户信息
5. 缓存未命中 → 调用 accounts-service/verify
6. 远程验证成功 → 设置缓存 → 返回用户信息
7. 验证失败 → 返回 401
8. 存储用户信息到 Gin Context
9. 业务逻辑处理
```
### 2. 缓存策略
- **TTL**: 60s
- **GC**: 5 分钟间隔
- **存储**: 内存哈希表
- **并发安全**: RWMutex
### 3. 配置示例
```yaml
# config/server.yaml
auth:
enable: true
authUrl: "https://accounts.svc.plus"
apiBaseUrl: "https://api.svc.plus"
publicToken: "xcontrol-public-token-2025"
```
### 4. 启用中间件
```go
// cmd/xcontrol-server/main.go
// 创建认证客户端
authConfig := auth.DefaultConfig()
authConfig.AuthURL = cfg.Auth.AuthURL
authConfig.PublicToken = cfg.Auth.PublicToken
authClient := auth.NewAuthClient(authConfig)
// 创建中间件配置
middlewareConfig := auth.DefaultMiddlewareConfig(authClient)
// 应用全局中间件
r.Use(auth.VerifyTokenMiddleware(middlewareConfig))
```
## 🎯 使用示例
### 基本认证
```go
r := gin.Default()
r.Use(auth.VerifyTokenMiddleware(middlewareConfig))
r.GET("/api/data", func(c *gin.Context) {
userID := auth.GetUserID(c)
email := auth.GetEmail(c)
c.JSON(http.StatusOK, gin.H{
"user_id": userID,
"email": email,
})
})
```
### 角色验证
```go
// 需要 admin 角色
r.GET("/admin", auth.RequireRole("admin"), handler)
// 需要 admin 或 moderator 角色
r.GET("/moderate", auth.RequireAnyRole("admin", "moderator"), handler)
```
### 跳过认证
```go
// 在中间件配置中
middlewareConfig := &auth.MiddlewareConfig{
SkipPaths: []string{
"/healthz",
"/ping",
"/metrics",
},
}
```
## 🔐 安全特性
1. **零持有私钥**
- 仅配置 publicToken
- 所有验证委托给 accounts-service
- 不存储敏感密钥
2. **Token 验证**
- Bearer token 格式检查
- 远程验证确保有效性
- 自动缓存减少延迟
3. **角色检查**
- 基于 JWT claims
- 支持单一角色验证
- 支持多角色任一匹配
4. **缓存安全**
- TTL 过期自动清理
- 后台 GC 防止内存泄漏
- 并发安全访问
## 📊 性能指标
- **缓存命中率**: 预期 > 80%
- **验证延迟**: 缓存命中 < 1ms远程验证 < 100ms
- **内存占用**: 约 10KB/1000 缓存条目
- **GC 开销**: < 1% CPU
## 🧪 测试
### 单元测试
```bash
cd rag-server/internal/auth
go test -v -bench=.
```
### 集成测试
```go
func TestAuthFlow(t *testing.T) {
authClient := auth.NewAuthClient(config)
resp, err := authClient.VerifyToken("valid_token")
assert.NoError(t, err)
assert.True(t, resp.Valid)
}
```
## 🚀 部署
1. **更新配置**
```yaml
# 确保 server.yaml 包含正确配置
auth:
enable: true
authUrl: "https://accounts.svc.plus"
publicToken: "xcontrol-public-token-2025"
```
2. **启动服务**
```bash
cd rag-server/cmd/xcontrol-server
go run main.go --config ../config/server.yaml
```
3. **验证认证**
```bash
# 健康检查
curl https://api.svc.plus/healthz
# 带认证的请求
curl -H "Authorization: Bearer <token>" \
https://api.svc.plus/api/data
```
## 📝 注意事项
1. **环境变量**
- 可以通过环境变量覆盖配置
- `AUTH_URL`, `PUBLIC_TOKEN`
2. **超时设置**
- 默认 10s 请求超时
- 可通过 `auth.DefaultConfig().Timeout` 调整
3. **错误处理**
- 所有错误返回标准 JSON 格式
- 区分 401 和 403 错误
4. **监控指标**
- `/healthz` 端点检查认证服务状态
- `auth.Cache.Stats()` 获取缓存统计
## ✅ 验证清单
- [x] 客户端调用 accounts-service 验证 token
- [x] 缓存验证结果 60s
- [x] 支持 Gin 中间件
- [x] 所有请求携带 Authorization header
- [x] 不持有 accessSecret/refreshSecret
- [x] 返回 JSON 格式错误
- [x] Go ≥1.24 兼容
- [x] 补充 config/server.yaml
## 🔗 相关文档
- [account-service API 文档](../../account/api/README.md)
- [JWT 认证最佳实践](../../docs/JWT_AUTH.md)
- [安全配置指南](../../docs/SECURITY.md)

View File

@ -1,406 +0,0 @@
# RAG Server Authentication Middleware
rag-server 认证中间件实现,用于验证访问者身份。所有认证请求都委托给 accounts-service 处理。
## 特性
- ✅ 远程验证:调用 accounts-service 验证 token
- ✅ 缓存机制:缓存验证结果 60s减少远程调用
- ✅ 零持有:不持有 accessSecret/refreshSecret
- ✅ Gin 集成:与 Gin Web 框架无缝集成
- ✅ 角色验证:支持角色检查
- ✅ 健康检查:内置健康检查端点
## 架构
```
┌─────────────┐ HTTP GET /verify ┌──────────────────┐
│ │─────────────────────────────────→│ │
│ rag-server │ │ accounts-service │
│ │←─────────────────────────────────│ │
└─────────────┘ Token Verify Response └──────────────────┘
│ HTTP Request
┌─────────────┐
│ Client │
└─────────────┘
```
## 配置
### 1. server.yaml 配置
```yaml
auth:
enable: true # 启用认证
authUrl: "https://accounts.svc.plus" # accounts-service 地址
apiBaseUrl: "https://api.svc.plus" # API 基础地址
publicToken: "xcontrol-public-token-2025" # 公钥(仅此密钥)
```
### 2. main.go 启用中间件
```go
package main
import (
"xcontrol/rag-server/internal/auth"
)
func main() {
// ... 加载配置
// 创建认证客户端
authConfig := auth.DefaultConfig()
authConfig.AuthURL = cfg.Auth.AuthURL
authConfig.PublicToken = cfg.Auth.PublicToken
authClient := auth.NewAuthClient(authConfig)
// 创建中间件配置
middlewareConfig := auth.DefaultMiddlewareConfig(authClient)
// 应用全局中间件
r.Use(auth.VerifyTokenMiddleware(middlewareConfig))
// 添加健康检查
r.GET("/healthz", auth.HealthCheckHandler(authClient))
}
```
## 使用方法
### 1. 基本认证
所有带有 `Authorization: Bearer <token>` 的请求都会被自动验证:
```go
r := gin.Default()
r.Use(auth.VerifyTokenMiddleware(middlewareConfig))
// 需要认证的路由
r.GET("/api/data", func(c *gin.Context) {
userID := auth.GetUserID(c)
email := auth.GetEmail(c)
roles := auth.GetRoles(c)
c.JSON(http.StatusOK, gin.H{
"user_id": userID,
"email": email,
"roles": roles,
})
})
```
客户端请求:
```bash
curl -H "Authorization: Bearer <access_token>" \
https://api.svc.plus/api/data
```
### 2. 角色验证
#### 单一角色检查
```go
r.GET("/admin", auth.RequireRole("admin"), func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "Admin access granted",
})
})
```
#### 多个角色检查(任一匹配)
```go
r.GET("/moderator", auth.RequireAnyRole("admin", "moderator"), func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "Moderator access granted",
})
})
```
### 3. 跳过认证
#### 全局跳过路径
在中间件配置中设置 `SkipPaths`
```go
middlewareConfig := &auth.MiddlewareConfig{
AuthClient: client,
Cache: auth.NewTokenCache(nil),
SkipPaths: []string{
"/healthz",
"/ping",
"/api/auth/",
},
}
```
#### 分组跳过
```go
publicGroup := r.Group("/api/public")
publicGroup.Use(auth.VerifyTokenMiddleware(middlewareConfig))
// 这个分组中的所有路由都会跳过认证
```
### 4. 缓存配置
```go
cacheConfig := &auth.CacheConfig{
TTL: 60 * time.Second, // 默认 60s
GCInterval: 5 * time.Minute, // 垃圾回收间隔
InitialSize: 100, // 初始容量
}
cache := auth.NewTokenCache(cacheConfig)
middlewareConfig := &auth.MiddlewareConfig{
AuthClient: client,
Cache: cache,
}
```
## API 参考
### 中间件函数
#### VerifyTokenMiddleware(config)
创建认证中间件。
**参数:**
- `config`: `*MiddlewareConfig` - 中间件配置
**示例:**
```go
middleware := auth.VerifyTokenMiddleware(config)
r.Use(middleware)
```
#### RequireRole(role)
检查用户是否具有特定角色。
**参数:**
- `role`: `string` - 所需角色
**示例:**
```go
r.GET("/admin", auth.RequireRole("admin"), handler)
```
#### RequireAnyRole(roles...)
检查用户是否具有任一指定角色。
**参数:**
- `roles`: `...string` - 允许的角色列表
**示例:**
```go
r.GET("/moderate", auth.RequireAnyRole("admin", "moderator"), handler)
```
### 辅助函数
#### GetUserID(c)
从 Gin 上下文获取用户 ID。
**示例:**
```go
userID := auth.GetUserID(c)
```
#### GetEmail(c)
从 Gin 上下文获取用户邮箱。
**示例:**
```go
email := auth.GetEmail(c)
```
#### GetRoles(c)
从 Gin 上下文获取用户角色列表。
**示例:**
```go
roles := auth.GetRoles(c)
```
### 健康检查
#### HealthCheckHandler(client)
创建健康检查处理器。
**示例:**
```go
r.GET("/healthz", auth.HealthCheckHandler(authClient))
```
响应示例:
```json
{
"status": "ok",
"message": "auth service healthy"
}
```
## 错误处理
中间件返回标准化的错误响应:
### 401 Unauthorized
```json
{
"error": "unauthorized",
"message": "missing authorization header"
}
```
### 403 Forbidden
```json
{
"error": "forbidden",
"message": "insufficient permissions",
"required_role": "admin"
}
```
### 503 Service Unavailable
```json
{
"status": "degraded",
"message": "auth service unavailable",
"detail": "connection timeout"
}
```
## 最佳实践
1. **使用健康检查**
```go
r.GET("/healthz", auth.HealthCheckHandler(client))
```
2. **合理设置缓存 TTL**
```go
// 60s 缓存适合大多数场景
cacheConfig := &auth.CacheConfig{
TTL: 60 * time.Second,
}
```
3. **跳过公共路径**
```go
middlewareConfig := auth.DefaultMiddlewareConfig(client)
middlewareConfig.SkipPaths = []string{
"/healthz",
"/ping",
"/metrics",
}
```
4. **错误日志记录**
```go
r.Use(gin.Logger())
r.Use(gin.Recovery())
```
## 测试
### 单元测试
```go
func TestVerifyTokenMiddleware(t *testing.T) {
// 设置测试环境
r := gin.Default()
middleware := auth.VerifyTokenMiddleware(config)
r.Use(middleware)
// 模拟请求
req, _ := http.NewRequest("GET", "/test", nil)
req.Header.Set("Authorization", "Bearer test_token")
// 执行测试
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
}
```
### 集成测试
```go
func TestAuthFlow(t *testing.T) {
// 1. 创建认证客户端
authClient := auth.NewAuthClient(config)
// 2. 验证 token
resp, err := authClient.VerifyToken("valid_token")
assert.NoError(t, err)
assert.True(t, resp.Valid)
// 3. 刷新 token
refreshResp, err := authClient.RefreshToken("refresh_token")
assert.NoError(t, err)
assert.NotEmpty(t, refreshResp.AccessToken)
}
```
## 性能优化
1. **缓存热点**:高频访问的 token 会自动缓存
2. **后台 GC**:自动清理过期缓存条目
3. **连接复用**HTTP 客户端复用连接
4. **超时控制**:可配置请求超时时间
## 故障排除
### 问题 1认证失败
**症状:** 所有请求返回 401
**排查:**
1. 检查 token 是否有效
2. 检查 Authorization header 格式
3. 检查 accounts-service 是否可访问
### 问题 2角色检查失败
**症状:** 返回 403 Forbidden
**排查:**
1. 检查 token 中的角色信息
2. 检查角色检查函数
3. 检查角色字符串格式(逗号分隔)
### 问题 3缓存不生效
**症状:** 性能差,频繁远程调用
**排查:**
1. 检查缓存配置
2. 检查缓存是否初始化
3. 检查 GC 间隔设置
## 依赖
- Go ≥ 1.24
- Gin v2
- golang-jwt/jwt/v5
## 许可证
MIT

View File

@ -1,174 +0,0 @@
package auth
import (
"sync"
"time"
)
// CacheEntry 缓存条目
type CacheEntry struct {
Value *TokenVerifyResponse
ExpiresAt time.Time
}
// TokenCache Token 验证结果缓存
type TokenCache struct {
mu sync.RWMutex
cache map[string]*CacheEntry
gcInterval time.Duration
ttl time.Duration
quit chan struct{}
}
// Config 缓存配置
type CacheConfig struct {
TTL time.Duration // 默认 60s
GCInterval time.Duration // 垃圾回收间隔,默认 5m
InitialSize int // 初始容量,默认 100
}
// DefaultCacheConfig 返回默认缓存配置
func DefaultCacheConfig() *CacheConfig {
return &CacheConfig{
TTL: 60 * time.Second,
GCInterval: 5 * time.Minute,
InitialSize: 100,
}
}
// NewTokenCache 创建新的 Token 缓存
func NewTokenCache(cfg *CacheConfig) *TokenCache {
if cfg == nil {
cfg = DefaultCacheConfig()
}
if cfg.TTL == 0 {
cfg.TTL = 60 * time.Second
}
if cfg.GCInterval == 0 {
cfg.GCInterval = 5 * time.Minute
}
if cfg.InitialSize == 0 {
cfg.InitialSize = 100
}
cache := &TokenCache{
cache: make(map[string]*CacheEntry, cfg.InitialSize),
gcInterval: cfg.GCInterval,
ttl: cfg.TTL,
quit: make(chan struct{}),
}
// 启动后台 GC 任务
go cache.gcWorker()
return cache
}
// Get 获取缓存的验证结果
func (c *TokenCache) Get(token string) (*TokenVerifyResponse, bool) {
c.mu.RLock()
entry, exists := c.cache[token]
c.mu.RUnlock()
if !exists {
return nil, false
}
// 检查是否过期
if time.Now().After(entry.ExpiresAt) {
// 异步删除过期条目
go c.Delete(token)
return nil, false
}
return entry.Value, true
}
// Set 设置缓存
func (c *TokenCache) Set(token string, value *TokenVerifyResponse) {
c.mu.Lock()
c.cache[token] = &CacheEntry{
Value: value,
ExpiresAt: time.Now().Add(c.ttl),
}
c.mu.Unlock()
}
// Delete 删除缓存
func (c *TokenCache) Delete(token string) {
c.mu.Lock()
delete(c.cache, token)
c.mu.Unlock()
}
// Clear 清空缓存
func (c *TokenCache) Clear() {
c.mu.Lock()
for key := range c.cache {
delete(c.cache, key)
}
c.mu.Unlock()
}
// Size 返回缓存大小
func (c *TokenCache) Size() int {
c.mu.RLock()
size := len(c.cache)
c.mu.RUnlock()
return size
}
// Stop 停止缓存清理任务
func (c *TokenCache) Stop() {
close(c.quit)
}
// gcWorker 后台垃圾回收工作协程
func (c *TokenCache) gcWorker() {
ticker := time.NewTicker(c.gcInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
c.gc()
case <-c.quit:
return
}
}
}
// gc 清理过期缓存
func (c *TokenCache) gc() {
now := time.Now()
c.mu.Lock()
for token, entry := range c.cache {
if now.After(entry.ExpiresAt) {
delete(c.cache, token)
}
}
c.mu.Unlock()
}
// Stats 缓存统计信息
type CacheStats struct {
Size int `json:"size"`
TTL time.Duration `json:"ttl"`
GCInterval time.Duration `json:"gc_interval"`
HitCount int64 `json:"hit_count"`
MissCount int64 `json:"miss_count"`
EvictionCount int64 `json:"eviction_count"`
}
// Stats 返回缓存统计信息
func (c *TokenCache) Stats() CacheStats {
return CacheStats{
Size: c.Size(),
TTL: c.ttl,
GCInterval: c.gcInterval,
}
}

View File

@ -1,258 +0,0 @@
package auth
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"time"
)
// AuthClient 用于调用 accounts-service 的认证接口
type AuthClient struct {
authURL string
publicToken string
httpClient *http.Client
timeout time.Duration
}
// Config 认证客户端配置
type Config struct {
AuthURL string
PublicToken string
Timeout time.Duration
MaxRetries int
RetryDelay time.Duration
}
// DefaultConfig 返回默认配置
func DefaultConfig() *Config {
return &Config{
Timeout: 10 * time.Second,
MaxRetries: 3,
RetryDelay: 500 * time.Millisecond,
}
}
// NewAuthClient 创建新的认证客户端
func NewAuthClient(cfg *Config) *AuthClient {
if cfg == nil {
cfg = DefaultConfig()
}
if cfg.Timeout == 0 {
cfg.Timeout = 10 * time.Second
}
return &AuthClient{
authURL: cfg.AuthURL,
publicToken: cfg.PublicToken,
httpClient: &http.Client{
Timeout: cfg.Timeout,
},
timeout: cfg.Timeout,
}
}
// TokenVerifyResponse 验证 token 的响应
type TokenVerifyResponse struct {
Valid bool `json:"valid"`
UserID string `json:"user_id"`
Email string `json:"email"`
Roles string `json:"roles"`
}
// ExchangeRequest 交换 token 请求
type ExchangeRequest struct {
PublicToken string `json:"public_token"`
UserID string `json:"user_id"`
Email string `json:"email"`
Roles string `json:"roles"`
}
// ExchangeResponse 交换 token 响应
type ExchangeResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
}
// RefreshRequest 刷新 token 请求
type RefreshRequest struct {
RefreshToken string `json:"refresh_token"`
}
// RefreshResponse 刷新 token 响应
type RefreshResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
}
// ErrorResponse 标准错误响应
type ErrorResponse struct {
Error string `json:"error"`
Message string `json:"message"`
}
// VerifyToken 验证 access token
func (c *AuthClient) VerifyToken(token string) (*TokenVerifyResponse, error) {
if token == "" {
return nil, fmt.Errorf("token is required")
}
req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/auth/verify", c.authURL), nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to verify token: %w", err)
}
defer resp.Body.Close()
// 解析响应
if resp.StatusCode == http.StatusUnauthorized {
return &TokenVerifyResponse{
Valid: false,
}, nil
}
if resp.StatusCode != http.StatusOK {
var errorResp ErrorResponse
if err := json.NewDecoder(resp.Body).Decode(&errorResp); err != nil {
return nil, fmt.Errorf("token verification failed with status %d", resp.StatusCode)
}
return nil, fmt.Errorf("verification failed: %s", errorResp.Message)
}
var verifyResp TokenVerifyResponse
if err := json.NewDecoder(resp.Body).Decode(&verifyResp); err != nil {
return nil, fmt.Errorf("failed to decode verify response: %w", err)
}
return &verifyResp, nil
}
// ExchangeToken 使用 publicToken 换取 token 对
func (c *AuthClient) ExchangeToken(userID, email, roles string) (*ExchangeResponse, error) {
if c.publicToken == "" {
return nil, fmt.Errorf("public token is required")
}
payload := ExchangeRequest{
PublicToken: c.publicToken,
UserID: userID,
Email: email,
Roles: roles,
}
jsonPayload, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("failed to marshal payload: %w", err)
}
req, err := http.NewRequest("POST", fmt.Sprintf("%s/api/auth/exchange", c.authURL), bytes.NewBuffer(jsonPayload))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to exchange token: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
var errorResp ErrorResponse
if err := json.NewDecoder(resp.Body).Decode(&errorResp); err != nil {
return nil, fmt.Errorf("token exchange failed with status %d", resp.StatusCode)
}
return nil, fmt.Errorf("exchange failed: %s", errorResp.Message)
}
var exchangeResp ExchangeResponse
if err := json.NewDecoder(resp.Body).Decode(&exchangeResp); err != nil {
return nil, fmt.Errorf("failed to decode exchange response: %w", err)
}
return &exchangeResp, nil
}
// RefreshToken 刷新 access token
func (c *AuthClient) RefreshToken(refreshToken string) (*RefreshResponse, error) {
if refreshToken == "" {
return nil, fmt.Errorf("refresh token is required")
}
payload := RefreshRequest{
RefreshToken: refreshToken,
}
jsonPayload, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("failed to marshal payload: %w", err)
}
req, err := http.NewRequest("POST", fmt.Sprintf("%s/api/auth/refresh", c.authURL), bytes.NewBuffer(jsonPayload))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to refresh token: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
var errorResp ErrorResponse
if err := json.NewDecoder(resp.Body).Decode(&errorResp); err != nil {
return nil, fmt.Errorf("token refresh failed with status %d", resp.StatusCode)
}
return nil, fmt.Errorf("refresh failed: %s", errorResp.Message)
}
var refreshResp RefreshResponse
if err := json.NewDecoder(resp.Body).Decode(&refreshResp); err != nil {
return nil, fmt.Errorf("failed to decode refresh response: %w", err)
}
return &refreshResp, nil
}
// HealthCheck 检查认证服务健康状态
func (c *AuthClient) HealthCheck() error {
req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/auth/self-check", c.authURL), nil)
if err != nil {
return fmt.Errorf("failed to create health check request: %w", err)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("health check failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("health check failed with status %d", resp.StatusCode)
}
return nil
}
// Close 关闭客户端
func (c *AuthClient) Close() {
if c.httpClient != nil {
c.httpClient.CloseIdleConnections()
}
}

View File

@ -1,119 +0,0 @@
package auth
import (
"net/http"
"testing"
"github.com/gin-gonic/gin"
)
// 示例:在业务路由中使用认证中间件
func ExampleRequireRole() {
// 创建认证客户端
authConfig := DefaultConfig()
authConfig.AuthURL = "https://accounts.svc.plus"
authConfig.PublicToken = "xcontrol-public-token-2025"
client := NewAuthClient(authConfig)
// 创建中间件配置
middlewareConfig := DefaultMiddlewareConfig(client)
// 创建 Gin 路由器
r := gin.Default()
// 需要认证的路由
authorized := r.Group("/api/v1")
authorized.Use(VerifyTokenMiddleware(middlewareConfig))
// 示例 1获取当前用户信息
authorized.GET("/me", func(c *gin.Context) {
userID := GetUserID(c)
email := GetEmail(c)
roles := GetRoles(c)
c.JSON(http.StatusOK, gin.H{
"user_id": userID,
"email": email,
"roles": roles,
})
})
// 示例 2需要特定角色的路由
authorized.GET("/admin", RequireRole("admin"), func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "Admin access granted",
})
})
// 示例 3需要任一角色的路由
authorized.GET("/moderator", RequireAnyRole("admin", "moderator"), func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "Moderator access granted",
})
})
// 示例 4不需要认证的路由使用 SkipPaths
r.GET("/healthz", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "ok",
})
})
// 示例 5自定义跳过认证的路由
publicGroup := r.Group("/api/v1/public")
publicGroup.Use(VerifyTokenMiddleware(&MiddlewareConfig{
AuthClient: client,
Cache: NewTokenCache(nil),
SkipPaths: []string{
"/api/v1/public/info",
},
}))
publicGroup.GET("/info", func(c *gin.Context) {
// 这个路由会跳过认证
c.JSON(http.StatusOK, gin.H{
"message": "Public info",
})
})
}
// 示例:健康检查
func ExampleHealthCheck() {
authConfig := DefaultConfig()
authConfig.AuthURL = "https://accounts.svc.plus"
authConfig.PublicToken = "xcontrol-public-token-2025"
client := NewAuthClient(authConfig)
r := gin.Default()
// 健康检查路由
r.GET("/healthz", HealthCheckHandler(client))
// 运行测试
_ = r
}
// Benchmark 示例
func BenchmarkVerifyTokenMiddleware(b *testing.B) {
authConfig := DefaultConfig()
authConfig.AuthURL = "https://accounts.svc.plus"
authConfig.PublicToken = "xcontrol-public-token-2025"
client := NewAuthClient(authConfig)
middlewareConfig := DefaultMiddlewareConfig(client)
// 设置测试路由
r := gin.New()
r.Use(VerifyTokenMiddleware(middlewareConfig))
r.GET("/test", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok"})
})
// 模拟请求(注意:这里没有真实的 token实际使用时需要有效的 token
b.ResetTimer()
for i := 0; i < b.N; i++ {
// 执行基准测试
_ = r
}
}

View File

@ -1,304 +0,0 @@
package auth
import (
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
)
// ContextKey 上下文键名
type ContextKey string
const (
UserIDKey ContextKey = "user_id"
EmailKey ContextKey = "email"
RolesKey ContextKey = "roles"
)
// UserContext 用户上下文信息
type UserContext struct {
UserID string `json:"user_id"`
Email string `json:"email"`
Roles []string `json:"roles"`
}
// MiddlewareConfig 中间件配置
type MiddlewareConfig struct {
AuthClient *AuthClient
Cache *TokenCache
SkipPaths []string // 跳过验证的路径
CacheTTL time.Duration
}
// DefaultMiddlewareConfig 返回默认中间件配置
func DefaultMiddlewareConfig(authClient *AuthClient) *MiddlewareConfig {
return &MiddlewareConfig{
AuthClient: authClient,
Cache: NewTokenCache(nil),
SkipPaths: []string{
"/healthz",
"/ping",
"/api/auth/",
},
CacheTTL: 60 * time.Second,
}
}
// VerifyTokenMiddleware 创建认证中间件
func VerifyTokenMiddleware(cfg *MiddlewareConfig) gin.HandlerFunc {
// 检查配置
if cfg == nil || cfg.AuthClient == nil {
panic("VerifyTokenMiddleware requires a valid AuthClient")
}
return func(c *gin.Context) {
// 检查是否跳过路径
for _, skipPath := range cfg.SkipPaths {
if strings.HasPrefix(c.Request.URL.Path, skipPath) {
c.Next()
return
}
}
// 获取 Authorization header
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"error": "unauthorized",
"message": "missing authorization header",
})
c.Abort()
return
}
// 验证 Bearer token 格式
if !strings.HasPrefix(authHeader, "Bearer ") {
c.JSON(http.StatusUnauthorized, gin.H{
"error": "unauthorized",
"message": "invalid authorization header format",
})
c.Abort()
return
}
token := strings.TrimPrefix(authHeader, "Bearer ")
token = strings.TrimSpace(token)
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"error": "unauthorized",
"message": "empty token",
})
c.Abort()
return
}
// 尝试从缓存获取验证结果
var userCtx *UserContext
cacheKey := token
if cfg.Cache != nil {
if cached, found := cfg.Cache.Get(cacheKey); found {
if cached != nil && cached.Valid {
userCtx = &UserContext{
UserID: cached.UserID,
Email: cached.Email,
Roles: []string{cached.Roles}, // 从字符串转换为字符串数组
}
}
}
}
// 如果缓存未命中,调用远程验证
if userCtx == nil {
verifyResp, err := cfg.AuthClient.VerifyToken(token)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"error": "unauthorized",
"message": "token verification failed",
"detail": err.Error(),
})
c.Abort()
return
}
// 验证失败
if !verifyResp.Valid {
c.JSON(http.StatusUnauthorized, gin.H{
"error": "unauthorized",
"message": "invalid or expired token",
})
c.Abort()
return
}
// 构建用户上下文
userCtx = &UserContext{
UserID: verifyResp.UserID,
Email: verifyResp.Email,
Roles: parseRoles(verifyResp.Roles),
}
// 写入缓存
if cfg.Cache != nil {
cfg.Cache.Set(cacheKey, verifyResp)
}
}
// 将用户信息存储到 Gin 上下文
c.Set(string(UserIDKey), userCtx.UserID)
c.Set(string(EmailKey), userCtx.Email)
c.Set(string(RolesKey), userCtx.Roles)
// 继续处理请求
c.Next()
}
}
// parseRoles 解析角色字符串为字符串数组
func parseRoles(rolesStr string) []string {
if rolesStr == "" {
return []string{"user"}
}
// 支持逗号分隔的角色列表
roles := strings.Split(rolesStr, ",")
var result []string
for _, role := range roles {
role = strings.TrimSpace(role)
if role != "" {
result = append(result, role)
}
}
if len(result) == 0 {
return []string{"user"}
}
return result
}
// GetUserID 从 Gin 上下文获取用户 ID
func GetUserID(c *gin.Context) string {
if value, exists := c.Get(string(UserIDKey)); exists {
if userID, ok := value.(string); ok {
return userID
}
}
return ""
}
// GetEmail 从 Gin 上下文获取邮箱
func GetEmail(c *gin.Context) string {
if value, exists := c.Get(string(EmailKey)); exists {
if email, ok := value.(string); ok {
return email
}
}
return ""
}
// GetRoles 从 Gin 上下文获取角色列表
func GetRoles(c *gin.Context) []string {
if value, exists := c.Get(string(RolesKey)); exists {
if roles, ok := value.([]string); ok {
return roles
}
}
return nil
}
// RequireRole 检查用户角色
func RequireRole(requiredRole string) gin.HandlerFunc {
return func(c *gin.Context) {
roles := GetRoles(c)
if len(roles) == 0 {
c.JSON(http.StatusForbidden, gin.H{
"error": "forbidden",
"message": "no roles found",
})
c.Abort()
return
}
// 检查是否包含所需角色
for _, role := range roles {
if role == requiredRole {
c.Next()
return
}
}
c.JSON(http.StatusForbidden, gin.H{
"error": "forbidden",
"message": "insufficient permissions",
"required_role": requiredRole,
})
c.Abort()
}
}
// RequireAnyRole 检查用户是否具有任一角色
func RequireAnyRole(allowedRoles ...string) gin.HandlerFunc {
if len(allowedRoles) == 0 {
panic("RequireAnyRole requires at least one role")
}
return func(c *gin.Context) {
roles := GetRoles(c)
if len(roles) == 0 {
c.JSON(http.StatusForbidden, gin.H{
"error": "forbidden",
"message": "no roles found",
})
c.Abort()
return
}
// 检查是否包含任一允许的角色
for _, role := range roles {
for _, allowed := range allowedRoles {
if role == allowed {
c.Next()
return
}
}
}
c.JSON(http.StatusForbidden, gin.H{
"error": "forbidden",
"message": "insufficient permissions",
"required_roles": allowedRoles,
})
c.Abort()
}
}
// HealthCheckHandler 健康检查处理器
func HealthCheckHandler(authClient *AuthClient) gin.HandlerFunc {
return func(c *gin.Context) {
if authClient == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{
"status": "degraded",
"message": "auth client not configured",
})
return
}
if err := authClient.HealthCheck(); err != nil {
c.JSON(http.StatusServiceUnavailable, gin.H{
"status": "degraded",
"message": "auth service unavailable",
"detail": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": "ok",
"message": "auth service healthy",
})
}
}