安全需要隔离, 隔离才能安全

Posted on Sat 19 July 2025 in Journal

Abstract Journal on 2025-07-19
Authors Walter Fan
 Category    learning note  
Status v1.0
Updated 2025-07-19
License CC-BY-NC-ND 4.0

安全需要隔离, 隔离才能安全

概述

在多租户和多用户环境,需要设计一个安全可靠的系统架构,确保用户数据完全隔离,防止跨用户访问和数据泄露。

核心安全原则

1. 数据隔离

  • 物理隔离: 不同租户的数据存储分离
  • 逻辑隔离: 通过租户ID和用户ID进行数据过滤
  • 访问控制: 严格的权限验证机制

2. 最小权限原则

  • 用户只能访问自己的数据
  • 系统组件只拥有必要的权限
  • 定期权限审查和更新

3. 深度防御

  • 多层安全验证
  • 输入验证和输出过滤
  • 审计日志和监控

系统架构设计

1. 用户认证和授权系统

// 用户模型
type User struct {
    ID           string    `json:"id" bson:"_id"`
    TenantID     string    `json:"tenant_id" bson:"tenant_id"`
    Username     string    `json:"username" bson:"username"`
    Email        string    `json:"email" bson:"email"`
    PasswordHash string    `json:"-" bson:"password_hash"`
    Role         string    `json:"role" bson:"role"` // admin, user, guest
    Status       string    `json:"status" bson:"status"` // active, inactive, suspended
    CreatedAt    time.Time `json:"created_at" bson:"created_at"`
    UpdatedAt    time.Time `json:"updated_at" bson:"updated_at"`
    LastLoginAt  time.Time `json:"last_login_at" bson:"last_login_at"`
}

// 租户模型
type Tenant struct {
    ID          string    `json:"id" bson:"_id"`
    Name        string    `json:"name" bson:"name"`
    Domain      string    `json:"domain" bson:"domain"`
    Status      string    `json:"status" bson:"status"` // active, suspended, deleted
    Plan        string    `json:"plan" bson:"plan"` // free, pro, enterprise
    MaxUsers    int       `json:"max_users" bson:"max_users"`
    MaxSessions int       `json:"max_sessions" bson:"max_sessions"`
    CreatedAt   time.Time `json:"created_at" bson:"created_at"`
    UpdatedAt   time.Time `json:"updated_at" bson:"updated_at"`
}

// JWT Claims
type Claims struct {
    UserID   string `json:"user_id"`
    TenantID string `json:"tenant_id"`
    Username string `json:"username"`
    Role     string `json:"role"`
    jwt.RegisteredClaims
}

2. 会话隔离设计

// 修改SessionMemory结构,添加租户和用户隔离
type SessionMemory struct {
    SessionID    string        `json:"session_id" bson:"session_id"`
    TenantID     string        `json:"tenant_id" bson:"tenant_id"`     // 新增:租户ID
    UserID       string        `json:"user_id" bson:"user_id"`         // 新增:用户ID
    Messages     []ChatMessage `json:"messages" bson:"messages"`
    TotalTokens  int           `json:"total_tokens" bson:"total_tokens"`
    CreatedAt    time.Time     `json:"created_at" bson:"created_at"`
    LastActivity time.Time     `json:"last_activity" bson:"last_activity"`
    IsPublic     bool          `json:"is_public" bson:"is_public"`     // 新增:是否公开
    SharedWith   []string      `json:"shared_with" bson:"shared_with"` // 新增:共享用户列表
    mu           sync.RWMutex  `json:"-"`
}

// 修改MemoryManager,支持多租户
type MemoryManager struct {
    sessions map[string]*SessionMemory
    mu       sync.RWMutex

    // 配置
    MaxTokens       int
    MaxMessages     int
    SummaryTokens   int
    SessionTimeout  time.Duration

    // 新增:租户限制
    MaxSessionsPerUser int
    MaxTokensPerTenant int
}

// 安全的会话获取方法
func (mm *MemoryManager) GetSession(sessionID, tenantID, userID string) (*SessionMemory, error) {
    mm.mu.Lock()
    defer mm.mu.Unlock()

    session, exists := mm.sessions[sessionID]
    if !exists {
        return nil, fmt.Errorf("session not found")
    }

    // 安全检查:确保用户只能访问自己的会话或共享会话
    if session.TenantID != tenantID {
        return nil, fmt.Errorf("access denied: tenant mismatch")
    }

    if session.UserID != userID && !session.IsPublic && !contains(session.SharedWith, userID) {
        return nil, fmt.Errorf("access denied: user not authorized")
    }

    session.LastActivity = time.Now()
    return session, nil
}

// 创建新会话的安全方法
func (mm *MemoryManager) CreateSession(sessionID, tenantID, userID string) (*SessionMemory, error) {
    mm.mu.Lock()
    defer mm.mu.Unlock()

    // 检查用户会话数量限制
    userSessions := mm.getUserSessions(tenantID, userID)
    if len(userSessions) >= mm.MaxSessionsPerUser {
        return nil, fmt.Errorf("user session limit exceeded")
    }

    session := &SessionMemory{
        SessionID:    sessionID,
        TenantID:     tenantID,
        UserID:       userID,
        Messages:     make([]ChatMessage, 0),
        TotalTokens:  0,
        CreatedAt:    time.Now(),
        LastActivity: time.Now(),
        IsPublic:     false,
        SharedWith:   make([]string, 0),
    }

    mm.sessions[sessionID] = session
    return session, nil
}

3. 中间件安全层

// 认证中间件
func AuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        // 从请求头获取JWT token
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "missing authorization header"})
            c.Abort()
            return
        }

        tokenString := strings.TrimPrefix(authHeader, "Bearer ")
        if tokenString == authHeader {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token format"})
            c.Abort()
            return
        }

        // 验证JWT token
        claims, err := validateToken(tokenString)
        if err != nil {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
            c.Abort()
            return
        }

        // 检查用户状态
        user, err := getUserByID(claims.UserID)
        if err != nil || user.Status != "active" {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "user not active"})
            c.Abort()
            return
        }

        // 检查租户状态
        tenant, err := getTenantByID(claims.TenantID)
        if err != nil || tenant.Status != "active" {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "tenant not active"})
            c.Abort()
            return
        }

        // 将用户信息存储到上下文中
        c.Set("user", user)
        c.Set("tenant", tenant)
        c.Set("claims", claims)

        c.Next()
    }
}

// 会话访问控制中间件
func SessionAccessMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        sessionID := c.Param("sessionId")
        if sessionID == "" {
            c.Next()
            return
        }

        claims := c.MustGet("claims").(*Claims)
        user := c.MustGet("user").(*User)

        // 验证会话访问权限
        memoryManager := mem.GetMemoryManager()
        session, err := memoryManager.GetSession(sessionID, claims.TenantID, claims.UserID)
        if err != nil {
            c.JSON(http.StatusForbidden, gin.H{"error": "access denied"})
            c.Abort()
            return
        }

        // 记录访问日志
        logAccess(user.ID, sessionID, "read")

        c.Set("session", session)
        c.Next()
    }
}

4. 安全的API端点

// 修改现有的API端点,添加安全验证
func handleChatRequest(c *gin.Context) {
    // 获取用户信息
    claims := c.MustGet("claims").(*Claims)
    user := c.MustGet("user").(*User)
    tenant := c.MustGet("tenant").(*Tenant)

    var req ChatAPIRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
        return
    }

    // 验证会话ID格式和权限
    if req.Remember && req.SessionId != "" {
        // 验证会话ID格式(防止遍历攻击)
        if !isValidSessionID(req.SessionId) {
            c.JSON(http.StatusBadRequest, gin.H{"error": "invalid session ID format"})
            return
        }

        // 确保会话ID包含用户信息
        if !strings.Contains(req.SessionId, user.ID) {
            c.JSON(http.StatusForbidden, gin.H{"error": "session ID must contain user identifier"})
            return
        }
    }

    // 构建安全的会话ID
    if req.Remember && req.SessionId == "" {
        req.SessionId = fmt.Sprintf("%s-%s-%s", tenant.ID, user.ID, generateSessionID())
    }

    // 处理请求(使用安全的方法)
    promptText, history, err := buildPromptTextWithMemory(&req, claims.TenantID, claims.UserID)
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    // ... 其余处理逻辑
}

// 安全的会话管理API
func getSessionInfo(c *gin.Context) {
    claims := c.MustGet("claims").(*Claims)
    sessionID := c.Param("sessionId")

    // 验证会话访问权限
    memoryManager := mem.GetMemoryManager()
    session, err := memoryManager.GetSession(sessionID, claims.TenantID, claims.UserID)
    if err != nil {
        c.JSON(http.StatusForbidden, gin.H{"error": "access denied"})
        return
    }

    // 只返回用户有权限看到的信息
    response := gin.H{
        "session": session.GetStats(),
        "messages": session.GetMessages(),
    }

    c.JSON(http.StatusOK, response)
}

// 列出用户自己的会话
func getUserSessions(c *gin.Context) {
    claims := c.MustGet("claims").(*Claims)

    memoryManager := mem.GetMemoryManager()
    sessions := memoryManager.GetUserSessions(claims.TenantID, claims.UserID)

    c.JSON(http.StatusOK, gin.H{"sessions": sessions})
}

5. 数据存储安全

// 数据库连接配置(支持多租户)
type DatabaseConfig struct {
    MasterDB   string `json:"master_db"`   // 主数据库(用户、租户信息)
    TenantDB   string `json:"tenant_db"`   // 租户数据数据库
    SessionDB  string `json:"session_db"`  // 会话数据数据库
}

// 安全的数据库查询
func getUserSessionsFromDB(tenantID, userID string) ([]SessionMemory, error) {
    // 使用参数化查询防止SQL注入
    query := `
        SELECT * FROM sessions 
        WHERE tenant_id = ? AND user_id = ? 
        ORDER BY last_activity DESC
    `

    rows, err := db.Query(query, tenantID, userID)
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var sessions []SessionMemory
    for rows.Next() {
        var session SessionMemory
        err := rows.Scan(&session.SessionID, &session.TenantID, &session.UserID, ...)
        if err != nil {
            return nil, err
        }
        sessions = append(sessions, session)
    }

    return sessions, nil
}

// Redis键设计(支持多租户)
func getSessionKey(tenantID, userID, sessionID string) string {
    return fmt.Sprintf("session:%s:%s:%s", tenantID, userID, sessionID)
}

func getUserSessionsKey(tenantID, userID string) string {
    return fmt.Sprintf("user_sessions:%s:%s", tenantID, userID)
}

6. 审计和监控

// 审计日志
type AuditLog struct {
    ID        string    `json:"id" bson:"_id"`
    TenantID  string    `json:"tenant_id" bson:"tenant_id"`
    UserID    string    `json:"user_id" bson:"user_id"`
    Action    string    `json:"action" bson:"action"` // create, read, update, delete
    Resource  string    `json:"resource" bson:"resource"` // session, user, tenant
    ResourceID string   `json:"resource_id" bson:"resource_id"`
    IP        string    `json:"ip" bson:"ip"`
    UserAgent string    `json:"user_agent" bson:"user_agent"`
    Timestamp time.Time `json:"timestamp" bson:"timestamp"`
    Success   bool      `json:"success" bson:"success"`
    Error     string    `json:"error,omitempty" bson:"error,omitempty"`
}

// 记录审计日志
func logAudit(tenantID, userID, action, resource, resourceID string, success bool, err error) {
    log := AuditLog{
        ID:         generateID(),
        TenantID:   tenantID,
        UserID:     userID,
        Action:     action,
        Resource:   resource,
        ResourceID: resourceID,
        IP:         getClientIP(),
        UserAgent:  getUserAgent(),
        Timestamp:  time.Now(),
        Success:    success,
    }

    if err != nil {
        log.Error = err.Error()
    }

    // 异步写入审计日志
    go func() {
        if err := auditDB.Insert(log); err != nil {
            log.GetLogger().Errorf("Failed to write audit log: %v", err)
        }
    }()
}

// 安全监控
func monitorSecurityEvents() {
    // 监控异常访问模式
    // 监控失败的认证尝试
    // 监控数据访问频率
    // 监控跨租户访问尝试
}

安全最佳实践

1. 输入验证

// 验证会话ID格式
func isValidSessionID(sessionID string) bool {
    // 只允许字母、数字、连字符和下划线
    matched, _ := regexp.MatchString(`^[a-zA-Z0-9_-]+$`, sessionID)
    return matched && len(sessionID) >= 10 && len(sessionID) <= 100
}

// 验证租户ID
func isValidTenantID(tenantID string) bool {
    matched, _ := regexp.MatchString(`^[a-zA-Z0-9_-]+$`, tenantID)
    return matched && len(tenantID) >= 5 && len(tenantID) <= 50
}

2. 输出过滤

// 过滤敏感信息
func sanitizeSessionData(session *SessionMemory, userID string) *SessionMemory {
    // 创建副本避免修改原始数据
    sanitized := *session

    // 如果不是会话所有者,隐藏某些信息
    if session.UserID != userID {
        sanitized.Messages = filterSensitiveMessages(session.Messages)
    }

    return &sanitized
}

3. 速率限制

// 基于租户和用户的速率限制
func rateLimitMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        claims := c.MustGet("claims").(*Claims)
        key := fmt.Sprintf("rate_limit:%s:%s", claims.TenantID, claims.UserID)

        if !checkRateLimit(key, 100, time.Minute) { // 每分钟100次请求
            c.JSON(http.StatusTooManyRequests, gin.H{"error": "rate limit exceeded"})
            c.Abort()
            return
        }

        c.Next()
    }
}

部署和运维安全

1. 环境隔离

  • 不同租户使用不同的数据库实例
  • 使用容器化部署,租户间网络隔离
  • 敏感数据加密存储

2. 监控告警

  • 实时监控异常访问模式
  • 设置安全事件告警
  • 定期安全审计

3. 备份和恢复

  • 定期备份租户数据
  • 测试数据恢复流程
  • 灾难恢复计划

总结

这个多租户安全系统设计确保了:

  1. 完全的数据隔离:用户只能访问自己的数据
  2. 严格的访问控制:多层安全验证机制
  3. 全面的审计:记录所有访问和操作
  4. 实时监控:及时发现和处理安全威胁
  5. 可扩展性:支持大量租户和用户

通过这样的设计,可以有效防止用户A访问用户B的会话,确保系统的安全性和可靠性。


本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。