如何在 go 中为每个租户使用一个 Keycloak 领域实现多租户

How to implement multitenancy with one Keycloak realm per tenant in go

目前我正在尝试通过为每个租户使用一个 Keycloak 领域来在 OAuth2 安全应用程序中实现多租户。我正在用 Go 创建一个原型,但并没有真正绑定到该语言,如果需要,可以切换到 Node.js 或 Java。我认为如果我切换语言,我的以下问题将成立。

起初,实施多租户对我来说似乎很简单:

根据指南,我使用 golang.org/x/oauth2 and github.com/coreos/go-oidc。这就是我为单个领域设置 OAuth 2 连接的方式:

provider, err := oidc.NewProvider(context.Background(), "http://keycloak.docker.localhost/auth/realms/tenant-1")
if err != nil {
    panic(err)
}

oauth2Config := oauth2.Config{
    ClientID:     "my-app",
    ClientSecret: "my-app-secret",
    RedirectURL:  "http://tenant-1.my-app.com/auth-callback",
    Endpoint: provider.Endpoint(),
    Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
}
state := "somestate"

verifier := provider.Verifier(&oidc.Config{
    ClientID: "my-app",
})

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    tenant, err := getTenantFromRequest(r)
    if err != nil {
        log.Println(err)
        w.WriteHeader(400)
        return
    }
    log.Printf("Received request for tenant %s\n", tenant)

    // Check if auth is present
    rawAccessToken := r.Header.Get("Authorization")
    if rawAccessToken == "" {
        log.Println("No Auth present, redirecting to auth code url...")
        http.Redirect(w, r, oauth2Config.AuthCodeURL(state), http.StatusFound)
        return
    }

    // Check if auth is valid
    parts := strings.Split(rawAccessToken, " ")
    if len(parts) != 2 {
        w.WriteHeader(400)
        return
    }
    _, err = verifier.Verify(context.Background(), parts[1])
    if err != nil {
        log.Printf("Error during auth verification (%s), redirecting to auth code url...\n", err)
        http.Redirect(w, r, oauth2Config.AuthCodeURL(state), http.StatusFound)
        return
    }

    // Authentication okay

    w.WriteHeader(200)
})

log.Printf("Starting server (%s)...\n", proxyConfig.Url)
log.Fatal(http.ListenAndServe(proxyConfig.Url, nil))

这工作正常,但现在是下一步,添加多租户。 IMO 似乎我需要为每个租户创建一个 oidc.Provider,因为需要在 Provider 结构中设置领域端点(http://keycloak.docker.localhost/auth/realms/tenant-1)。

我不确定这是否是处理这种情况的正确方法。我想我会为 oidc.Provider 个实例添加一个缓存,以避免在 每个 请求上创建实例。但是正在创建一个

尽管这还没有在生产中完全使用,但这是我如何制作一个我可能最终会使用的解决方案的原型:

我假设每个 keycloak 领域都必须有一个 oidc.Provider

因此每个领域都会有一个oidc.IDTokenVerifier

为了管理这些实例,我创建了这个界面:

// A mechanism that manages oidc.Provider and IDTokenVerifierInterface instances
type OAuth2ManagerInterface interface {
    GetOAuthProviderForKeycloakRealm(ctx context.Context, tenant string) (*oidc.Provider, error)
    GetOpenIdConnectVerifierForProvider(provider *oidc.Provider) (IDTokenVerifierInterface, error)
}

// Interface created to describe the oidc.IDTokenVerifier struct.
// This was created because the oidc modules does not define its own interface
type IDTokenVerifierInterface interface {
    Verify(ctx context.Context, rawIDToken string) (*oidc.IDToken, error)
}

那么这是将实现管理器接口的结构:

type OAuth2Manager struct {
    ProviderUrl string
    ClientId    string

    // Maps keycloak provider urls onto oidc.Provider instances
    // Used internally to avoid creating a new provider on each request
    providers map[string]*oidc.Provider

    // Lock to be used when accessing OAuth2Manager.providers
    providerLock sync.Mutex

    // Maps oidc.Provider instances onto oidc.IDTokenVerifier instances
    // Used internally to avoid creating a new verifier on each request
    verifiers map[*oidc.Provider]*oidc.IDTokenVerifier

    // Lock to be used when accessing OAuth2Manager.verifiers
    verifierLock sync.Mutex
}

连同实际实现接口的函数:

func (manager *OAuth2Manager) GetProviderUrlForRealm(realm string) string {
    return fmt.Sprintf("%s/%s", manager.ProviderUrl, realm)
}

func (manager *OAuth2Manager) GetOAuthProviderForKeycloakRealm(ctx context.Context, tenant string) (*oidc.Provider, error) {
    providerUrl := manager.GetProviderUrlForRealm(tenant)

    // Check if already exists ...
    manager.providerLock.Lock()
    if provider, alreadyExists := manager.providers[providerUrl]; alreadyExists {
        manager.providerLock.Unlock()
        return provider, nil
    }

    // ... create new instance if not
    provider, err := oidc.NewProvider(ctx, providerUrl)

    if err != nil {
        manager.providerLock.Unlock()
        return nil, err
    }

    manager.providers[providerUrl] = provider
    manager.providerLock.Unlock()

    log.Printf("Created new provider for provider url %s\n", providerUrl)

    return provider, nil
}

func (manager *OAuth2Manager) GetOpenIdConnectVerifierForProvider(provider *oidc.Provider) (IDTokenVerifierInterface, error) {
    // Check if already exists ...
    manager.verifierLock.Lock()
    if verifier, alreadyExists := manager.verifiers[provider]; alreadyExists {
        manager.verifierLock.Unlock()
        return verifier, nil
    }

    // ... create new instance if not
    oidcConfig := &oidc.Config{
        ClientID: manager.ClientId,
    }

    verifier := provider.Verifier(oidcConfig)

    manager.verifiers[provider] = verifier
    manager.verifierLock.Unlock()

    log.Printf("Created new verifier for OAuth endpoint %v\n", provider.Endpoint())

    return verifier, nil
}

如果并发访问提供者,使用 sync.Mutex 锁很重要。