# v1.3.0 - 安全最佳实践

## 输入验证

```go
package validator

import (
    "regexp"
    "unicode/utf8"
)

// ValidateUserID 验证用户ID
func ValidateUserID(userID int64) error {
    if userID <= 0 {
        return errors.New("invalid user_id: must be positive")
    }
    return nil
}

// ValidateMessage 验证消息内容
func ValidateMessage(content string) error {
    length := utf8.RuneCountInString(content)
    if length == 0 {
        return errors.New("message cannot be empty")
    }
    if length > 4096 {
        return errors.New("message too long: max 4096 characters")
    }
    
    // 检查危险字符
    dangerous := []string{"<script", "javascript:", "onerror", "onload"}
    lower := strings.ToLower(content)
    for _, d := range dangerous {
        if strings.Contains(lower, d) {
            return errors.New("message contains dangerous content")
        }
    }
    
    return nil
}

// ValidatePhone 验证手机号
func ValidatePhone(phone string) error {
    // E.164格式
    matched, _ := regexp.MatchString(`^\+[1-9]\d{1,14}$`, phone)
    if !matched {
        return errors.New("invalid phone number format")
    }
    return nil
}

// ValidateFileName 验证文件名
func ValidateFileName(name string) error {
    // 防止路径遍历
    if strings.Contains(name, "..") || strings.Contains(name, "/") || strings.Contains(name, "\\") {
        return errors.New("invalid filename: path traversal detected")
    }
    
    // 检查扩展名
    allowedExts := map[string]bool{
        ".jpg": true, ".jpeg": true, ".png": true, ".gif": true,
        ".mp4": true, ".mov": true, ".pdf": true, ".doc": true,
    }
    
    ext := strings.ToLower(filepath.Ext(name))
    if !allowedExts[ext] {
        return errors.New("file type not allowed")
    }
    
    return nil
}
```

## SQL注入防护

```go
package dao

// 使用参数化查询（永远不要用字符串拼接）

// ✅ 安全
func (d *DAO) GetUserSafe(userID int64) (*User, error) {
    query := "SELECT * FROM users WHERE id = ?"
    var user User
    err := d.db.QueryRow(query, userID).Scan(&user)
    return &user, err
}

// ❌ 危险 - SQL注入
func (d *DAO) GetUserUnsafe(userID string) (*User, error) {
    query := fmt.Sprintf("SELECT * FROM users WHERE id = %s", userID)
    // 攻击: userID = "1 OR 1=1"
    var user User
    err := d.db.QueryRow(query).Scan(&user)
    return &user, err
}

// IN查询安全处理
func (d *DAO) GetUsersByIDs(userIDs []int64) ([]*User, error) {
    query, args, err := sqlx.In("SELECT * FROM users WHERE id IN (?)", userIDs)
    if err != nil {
        return nil, err
    }
    query = d.db.Rebind(query)
    
    var users []*User
    err = d.db.Select(&users, query, args...)
    return users, err
}

// LIKE查询安全处理
func (d *DAO) SearchUsers(name string) ([]*User, error) {
    // 转义通配符
    safeName := strings.ReplaceAll(name, "%", "\\%")
    safeName = strings.ReplaceAll(safeName, "_", "\\_")
    
    query := "SELECT * FROM users WHERE name LIKE ? ESCAPE '\\'"
    pattern := "%" + safeName + "%"
    
    var users []*User
    err := d.db.Select(&users, query, pattern)
    return users, err
}
```

## XSS防护

```go
package security

import (
    "html"
    "strings"
)

// SanitizeHTML 转义HTML特殊字符
func SanitizeHTML(input string) string {
    return html.EscapeString(input)
}

// StripTags 移除HTML标签
func StripTags(input string) string {
    // 简单实现，生产环境使用bluemonday
    re := regexp.MustCompile(`<[^>]+>`)
    return re.ReplaceAllString(input, "")
}

// 消息内容处理
func ProcessMessageContent(content string) string {
    // 1. 转义HTML
    content = html.EscapeString(content)
    
    // 2. 保留允许的格式（Markdown-style）
    // **bold** → <b>bold</b>
    content = processBold(content)
    
    // 3. 链接处理
    content = processLinks(content)
    
    return content
}
```

## CSRF防护

```go
package middleware

// CSRF中间件
func CSRFMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 验证Origin头
        origin := r.Header.Get("Origin")
        if origin != "" && !isAllowedOrigin(origin) {
            http.Error(w, "Invalid origin", http.StatusForbidden)
            return
        }
        
        // 验证Referer
        referer := r.Header.Get("Referer")
        if referer != "" && !strings.HasPrefix(referer, allowedDomain) {
            http.Error(w, "Invalid referer", http.StatusForbidden)
            return
        }
        
        // 非安全方法需要验证CSRF Token
        if r.Method != "GET" && r.Method != "HEAD" {
            token := r.Header.Get("X-CSRF-Token")
            if !validateCSRFToken(r, token) {
                http.Error(w, "Invalid CSRF token", http.StatusForbidden)
                return
            }
        }
        
        next.ServeHTTP(w, r)
    })
}
```

## 敏感数据保护

```go
package security

import (
    "crypto/aes"
    "crypto/cipher"
    "crypto/rand"
    "encoding/base64"
    "io"
)

// 敏感字段加密（数据库中存储加密数据）
type SensitiveData struct {
    key []byte
}

func NewSensitiveData(key []byte) *SensitiveData {
    return &SensitiveData{key: key}
}

// Encrypt 加密敏感数据
func (s *SensitiveData) Encrypt(plaintext string) (string, error) {
    block, err := aes.NewCipher(s.key)
    if err != nil {
        return "", err
    }
    
    gcm, err := cipher.NewGCM(block)
    if err != nil {
        return "", err
    }
    
    nonce := make([]byte, gcm.NonceSize())
    if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
        return "", err
    }
    
    ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil)
    return base64.StdEncoding.EncodeToString(ciphertext), nil
}

// Decrypt 解密敏感数据
func (s *SensitiveData) Decrypt(ciphertext string) (string, error) {
    data, err := base64.StdEncoding.DecodeString(ciphertext)
    if err != nil {
        return "", err
    }
    
    block, err := aes.NewCipher(s.key)
    if err != nil {
        return "", err
    }
    
    gcm, err := cipher.NewGCM(block)
    if err != nil {
        return "", err
    }
    
    nonceSize := gcm.NonceSize()
    if len(data) < nonceSize {
        return "", errors.New("ciphertext too short")
    }
    
    nonce, ciphertext := data[:nonceSize], data[nonceSize:]
    plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
    if err != nil {
        return "", err
    }
    
    return string(plaintext), nil
}

// 使用示例
func (d *DAO) SaveUserPhone(userID int64, phone string) error {
    // 加密手机号后存储
    encrypted, err := sensitiveData.Encrypt(phone)
    if err != nil {
        return err
    }
    
    query := "UPDATE users SET phone_encrypted = ? WHERE id = ?"
    _, err = d.db.Exec(query, encrypted, userID)
    return err
}
```

## 密钥管理

```go
package security

import (
    "os"
    "github.com/hashicorp/vault/api"
)

// Vault密钥管理
type VaultManager struct {
    client *api.Client
}

func NewVaultManager(addr, token string) (*VaultManager, error) {
    config := &api.Config{
        Address: addr,
    }
    
    client, err := api.NewClient(config)
    if err != nil {
        return nil, err
    }
    
    client.SetToken(token)
    
    return &VaultManager{client: client}, nil
}

func (v *VaultManager) GetSecret(path string) (map[string]interface{}, error) {
    secret, err := v.client.Logical().Read(path)
    if err != nil {
        return nil, err
    }
    
    if secret == nil {
        return nil, errors.New("secret not found")
    }
    
    return secret.Data, nil
}

// 本地开发环境密钥加载
func LoadSecret(keyName string) string {
    // 1. 尝试从环境变量读取
    if val := os.Getenv(keyName); val != "" {
        return val
    }
    
    // 2. 尝试从文件读取（Docker Secrets等）
    if data, err := os.ReadFile("/run/secrets/" + keyName); err == nil {
        return string(data)
    }
    
    // 3. 开发环境使用默认值（生产环境不允许）
    if os.Getenv("ENV") == "development" {
        return getDevSecret(keyName)
    }
    
    log.Fatal("Secret not found: ", keyName)
    return ""
}
```

## 安全Headers

```go
package middleware

func SecurityHeaders(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 内容安全策略
        w.Header().Set("Content-Security-Policy", 
            "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'")
        
        // 禁用MIME类型嗅探
        w.Header().Set("X-Content-Type-Options", "nosniff")
        
        // XSS保护
        w.Header().Set("X-XSS-Protection", "1; mode=block")
        
        // 点击劫持保护
        w.Header().Set("X-Frame-Options", "DENY")
        
        // HSTS（HTTPS强制）
        w.Header().Set("Strict-Transport-Security", 
            "max-age=31536000; includeSubDomains; preload")
        
        //  Referrer策略
        w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
        
        // 权限策略
        w.Header().Set("Permissions-Policy", 
            "geolocation=(), microphone=(), camera=()")
        
        next.ServeHTTP(w, r)
    })
}
```

## 审计日志

```go
package audit

import (
    "context"
    "encoding/json"
    "time"
)

type AuditEvent struct {
    Timestamp   time.Time              `json:"timestamp"`
    UserID      int64                  `json:"user_id"`
    Action      string                 `json:"action"`
    Resource    string                 `json:"resource"`
    ResourceID  int64                  `json:"resource_id"`
    Status      string                 `json:"status"`
    IP          string                 `json:"ip"`
    UserAgent   string                 `json:"user_agent"`
    Details     map[string]interface{} `json:"details"`
}

type Auditor struct {
    producer KafkaProducer
}

func (a *Auditor) Log(ctx context.Context, event *AuditEvent) {
    event.Timestamp = time.Now()
    
    // 异步发送审计日志
    go func() {
        data, _ := json.Marshal(event)
        a.producer.Publish("audit.events", data)
    }()
}

// 使用示例
func (s *Server) DeleteMessage(ctx context.Context, req *DeleteMessageReq) (*Bool, error) {
    // 执行删除
    err := s.core.DeleteMessage(ctx, req.ChatID, req.MessageID)
    
    // 记录审计日志
    s.auditor.Log(ctx, &audit.AuditEvent{
        UserID:     getUserID(ctx),
        Action:     "DELETE",
        Resource:   "message",
        ResourceID: req.MessageID,
        Status:     getStatus(err),
        IP:         getClientIP(ctx),
        UserAgent:  getUserAgent(ctx),
        Details: map[string]interface{}{
            "chat_id": req.ChatID,
            "reason":  req.Reason,
        },
    })
    
    return BoolTrue, err
}
```
