Go 實現多租戶示例

在 Go 中實現多租戶 (multi-tenancy) 通常涉及到下面幾個關鍵步驟:

  1. 租戶識別: 你需要一個機制來區分請求是針對哪個租戶的。這可以通過多種策略實現,比如在 HTTP 請求的 URL、Header 或者是 Cookie 中嵌入租戶 ID。

  2. 數據隔離: 根據你選擇的數據隔離策略(如數據庫、schema 或者數據表的隔離),你需要確保租戶只能訪問到屬於他們的數據。

  3. 中間件 / 攔截器: 在處理請求的過程中,可以引入中間件或者攔截器來確保租戶的隔離策略得到執行。例如,在處理請求之前,中間件可以解析租戶 ID,並設置上下文 (context) 來確保後續的數據庫查詢和操作都是在正確的租戶上下文中進行的。

  4. 服務層: 在服務層(業務邏輯層)確保所有的數據訪問都根據當前的租戶上下文來進行。

  5. 數據庫連接: 爲每個租戶提供一個安全的數據庫連接,這可能涉及創建租戶特定的數據庫連接池。

Go 代碼示例

package main
import (
  "context"
  "fmt"
  "net/http"
)
// 租戶上下文鍵
type contextKey string
var tenantKey contextKey = "tenant"
// 中間件以從請求中提取租戶ID,並將租戶ID設置到context中
func tenantMiddleware(next http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    tenantID := r.Header.Get("X-Tenant-ID") // 假設租戶ID是通過HTTP Header傳入的
    if tenantID == "" {
      http.Error(w, "Tenant ID is required", http.StatusBadRequest)
      return
    }
    ctxWithTenantID := context.WithValue(r.Context(), tenantKey, tenantID)
    next.ServeHTTP(w, r.WithContext(ctxWithTenantID))
  })
}
// 數據庫查詢函數可以這樣使用上下文來獲取租戶ID
func queryDatabase(ctx context.Context) {
  tenantID, ok := ctx.Value(tenantKey).(string)
  if !ok {
    // 處理錯誤情況
    fmt.Println("Tenant ID not found in context")
    return
  }
  // 使用租戶ID來進行查詢...
  fmt.Printf("Querying database for tenant ID: %s\n", tenantID)
}
// 一個示例HTTP處理函數
func myHandler(w http.ResponseWriter, r *http.Request) {
  // 使用之前存儲在context中的租戶ID
  queryDatabase(r.Context())
  fmt.Fprintf(w, "Handled request for tenant\n")
}
func main() {
  http.Handle("/", tenantMiddleware(http.HandlerFunc(myHandler)))
  http.ListenAndServe(":8080", nil)
}

我們在 tenantMiddleware 函數中解析 HTTP 請求的租戶 ID,並將其設置到上下文(context)中。然後在服務的實際處理函數(myHandler)中,我們從上下文中獲取租戶 ID。在實際的數據庫操作中,queryDatabase 函數將根據上下文中的租戶 ID 來執行租戶特定的查詢。

上述是個簡單 Go 實現多租戶的例子,但是大家有沒有想過一個問題,既然多租戶 ID 是通過 header 傳進來的,在 ToC 開發中有一句話用戶的所有輸入都是不可信的,如果用戶篡改了 Header 頭的租戶 ID,是不是就可以拿到其他租戶的信息了。對此應該怎麼解決呢?

  1. 身份驗證和授權:確保每個請求都通過身份認證(Authentication)過程,併爲每個租戶的用戶分配適當的權限(Authorization)。這可以通過 OAuth2, JWT 等技術來實現。請注意,在解析租戶 ID 之前先進行用戶身份驗證。

  2. 租戶識別驗證:在租戶識別時,驗證確定租戶 ID 是否有效,是否與認證用戶的租戶 ID 相匹配,避免用戶嘗試訪問非授權租戶的數據。

  3. 數據庫設計:確保數據庫的設計能夠支持租戶數據的隔離。例如,您可以爲每個租戶使用獨立的數據庫,或在共享數據庫中使用 schemas,或在所有查詢中使用 tenant_id 作爲過濾條件來隔離數據。

基於上面的方案,需要增加如下的 Go 代碼

// 假設我們有一個函數來驗證請求是否已認證,並且返回已認證的用戶信息
func authenticateRequest(r *http.Request) (*UserInfo, error) {
    // 實現身份認證邏輯,失敗時返回錯誤
}
func tenantMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        userInfo, err := authenticateRequest(r)
        if err != nil {
            http.Error(w, "Authentication failed", http.StatusUnauthorized)
            return
        }
        tenantID := r.Header.Get("X-Tenant-ID")
        if tenantID == "" || userInfo.TenantID != tenantID {
            http.Error(w, "Invalid tenant ID", http.StatusBadRequest)
            return
        }
        ctxWithTenantID := context.WithValue(r.Context(), tenantKey, tenantID)
        next.ServeHTTP(w, r.WithContext(ctxWithTenantID))
    })
}
func queryDatabase(ctx context.Context) {
    tenantID, ok := ctx.Value(tenantKey).(string)
    if !ok {
        // 處理錯誤情況
        fmt.Println("Tenant ID not found in context")
        return
    }
    // 在執行數據庫操作之前,確保所有請求都使用tenantID過濾
    fmt.Printf("Querying database for tenant ID: %s\n", tenantID)
    // 執行實際的數據庫查詢,確保使用tenantID作爲查詢條件
    // ... 查詢邏輯 ...
}

在上面的例子中我們引入了認證機制來確保租戶的合理性。當然租戶還需要考慮其他的但是這個過程,剛開始不可能所有都想到隨着項目的演進多樣化的功能就會陸陸續續添加進來。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/H9XZPjy2JlK9QeAMy5-0UA