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:
parent
a1e4cc9146
commit
0b3fab7d28
@ -1,6 +0,0 @@
|
||||
package auth
|
||||
|
||||
// Provider defines a generic authentication provider.
|
||||
type Provider interface {
|
||||
Authenticate(username, password string) (string, error)
|
||||
}
|
||||
1
go.mod
1
go.mod
@ -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
2
go.sum
@ -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=
|
||||
|
||||
@ -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:启用 Fiber(Gin)中间件
|
||||
|
||||
**实现**: `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.24,Fiber 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*
|
||||
@ -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** - 启用 Fiber(Gin)中间件
|
||||
✅ **更新 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)
|
||||
@ -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
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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",
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user