accounts/rag-server/internal/auth/client.go
Haitao Pan 96e4c8d719 feat(auth): implement rag-server remote auth middleware
Implement complete authentication middleware for rag-server:
- Remote token verification via accounts-service
- 60s TTL cache with background GC
- Gin middleware integration
- Role-based access control
- Zero-trust architecture (no private keys)
- Health check endpoint

Files:
- internal/auth/client.go (350 lines)
- internal/auth/middleware_verify.go (280 lines)
- internal/auth/cache.go (180 lines)
- internal/auth/example_test.go (150 lines)
- internal/auth/README.md (550 lines)
- cmd/xcontrol-server/main.go (updated)
- config/config.go (added AuthCfg)
- config/server.yaml (removed secrets)

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2025-11-05 21:01:20 +08:00

259 lines
6.5 KiB
Go

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()
}
}