安全需要隔离, 隔离才能安全
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. 备份和恢复
- 定期备份租户数据
- 测试数据恢复流程
- 灾难恢复计划
总结
这个多租户安全系统设计确保了:
- 完全的数据隔离:用户只能访问自己的数据
- 严格的访问控制:多层安全验证机制
- 全面的审计:记录所有访问和操作
- 实时监控:及时发现和处理安全威胁
- 可扩展性:支持大量租户和用户
通过这样的设计,可以有效防止用户A访问用户B的会话,确保系统的安全性和可靠性。
本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。