多租户(x/tenant)
本指南展示如何使用 x/tenant 为 Plumego 服务添加多租户隔离。你将从每个请求中解析租户身份、在中间件层执行策略,并在 handler 和存储调用中读取租户 ID。
边界说明见 x/tenant Primer。
发布姿态说明
Section titled “发布姿态说明”x/tenant 是 experimental 且高风险的扩展家族,因为租户错误可能直接演变成隔离失败。租户 wiring 必须保持显式,并在 resolution、policy、quota、session 错误上 fail closed;在把它当作冻结的生产契约前,请先查看发布策略和扩展成熟度。
什么时候用 x/tenant,什么时候直接读 header
Section titled “什么时候用 x/tenant,什么时候直接读 header”如果你只需要从 header 中提取租户标识符并传给存储查询,可以直接在应用代码中完成,无需 x/tenant:
tenantID := r.Header.Get("X-Tenant-ID")rows, err := db.QueryContext(ctx, db, "SELECT * FROM items WHERE tenant_id = $1", tenantID)以下场景才需要 x/tenant:
- 强制执行、fail-closed 的租户解析(拒绝无租户的请求)
- 策略评估(每租户 feature flag、模型访问、工具白名单)
- 带
Retry-After反馈的每租户配额执行 - JWT 支持的租户会话生命周期
第一步 — 解析租户身份
Section titled “第一步 — 解析租户身份”在应用的中间件栈中添加 resolve.Middleware,放在 auth 之后以确保 JWT principal 已在 context 中:
import ( authmw "github.com/spcent/plumego/middleware/auth" resolve "github.com/spcent/plumego/x/tenant/resolve" "github.com/spcent/plumego/security/jwt")
// auth 先运行,JWT principal 才会可用authMw, err := authmw.Authenticate(jwtManager.Authenticator(jwt.TokenTypeAccess))if err != nil { return err}app.Use(authMw)
// 解析器先从 principal 提取租户 ID,再回退到 X-Tenant-ID headerapp.Use(resolve.Middleware(resolve.Options{ AllowMissing: false, // 拒绝无租户 ID 的请求(fail-closed)}))解析顺序:
- context 中
authn.Principal.TenantID(由 JWT 中间件设置) - 自定义
Extractor函数(如已配置) X-Tenant-IDHTTP header
使用自定义提取器
Section titled “使用自定义提取器”app.Use(resolve.Middleware(resolve.Options{ Extractor: func(r *http.Request) (string, error) { // 示例:从子域名提取 tenant.example.com host := r.Host parts := strings.SplitN(host, ".", 2) if len(parts) < 2 { return "", nil } return parts[0], nil }, AllowMissing: false,}))第二步 — 在 handler 中读取租户 ID
Section titled “第二步 — 在 handler 中读取租户 ID”import tenantcore "github.com/spcent/plumego/x/tenant/core"
func (h ItemHandler) List(w http.ResponseWriter, r *http.Request) { tenantID := tenantcore.TenantIDFromContext(r.Context())
items, err := h.db.QueryContext(r.Context(), "SELECT id, name FROM items WHERE tenant_id = $1", tenantID) // ...}第三步 — 执行租户策略(可选)
Section titled “第三步 — 执行租户策略(可选)”实现 tenantcore.PolicyEvaluator 并挂载 policy.Middleware:
import ( "github.com/spcent/plumego/x/tenant/policy" tenantcore "github.com/spcent/plumego/x/tenant/core")
type MyPolicyEvaluator struct { db *sql.DB}
func (e *MyPolicyEvaluator) Evaluate(ctx context.Context, tenantID string, req tenantcore.PolicyRequest) (tenantcore.PolicyResult, error) { // 在数据库中查找租户能力 allowed, err := e.isAllowed(ctx, tenantID, req.Path, req.Method) if err != nil { return tenantcore.PolicyResult{}, err } return tenantcore.PolicyResult{Allowed: allowed}, nil}
app.Use(policy.Middleware(policy.Options{ Evaluator: &MyPolicyEvaluator{db: sqlDB},}))第四步 — 执行每租户配额(可选)
Section titled “第四步 — 执行每租户配额(可选)”实现 tenantcore.QuotaManager 并挂载 quota.Middleware:
import ( "github.com/spcent/plumego/x/tenant/quota" tenantcore "github.com/spcent/plumego/x/tenant/core")
app.Use(quota.Middleware(quota.Options{ Manager: myQuotaManager, // 你的 tenantcore.QuotaManager 实现 Estimator: func(r *http.Request) int { // 从请求估算 token/请求消耗 return 1 },}))租户配额耗尽时,中间件返回 429,并附带 Retry-After 和 X-Quota-Remaining: 0。
在 auth 之后、路由 handler 之前挂载租户中间件:
app.Use(requestid.Middleware())recoveryMw, err := recovery.Middleware(recovery.Config{Logger: app.Logger()})if err != nil { return err}accesslogMw, err := accesslog.Middleware(accesslog.Config{Logger: app.Logger()})if err != nil { return err}app.Use(recoveryMw)app.Use(accesslogMw)authMw, err := authmw.Authenticate(authenticator)if err != nil { return err}app.Use(authMw) // auth 在租户之前app.Use(resolve.Middleware(resolveOpts)) // 租户解析app.Use(policy.Middleware(policyOpts)) // 策略在解析之后app.Use(quota.Middleware(quotaOpts)) // 配额在解析之后