如何在 go 中为每个租户使用一个 Keycloak 领域实现多租户
How to implement multitenancy with one Keycloak realm per tenant in go
目前我正在尝试通过为每个租户使用一个 Keycloak 领域来在 OAuth2 安全应用程序中实现多租户。我正在用 Go 创建一个原型,但并没有真正绑定到该语言,如果需要,可以切换到 Node.js 或 Java。我认为如果我切换语言,我的以下问题将成立。
起初,实施多租户对我来说似乎很简单:
- 为每个租户创建一个领域,其中包含我的后端应用程序所需的客户端配置。
- 后端收到带有 URL
tenant-1.my-app.com
的请求。解析 URL 以检索要用于身份验证的租户。
- 连接到 OAuth2 提供程序(在本例中为 Keycloak)并验证请求令牌。
根据指南,我使用 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
锁很重要。
目前我正在尝试通过为每个租户使用一个 Keycloak 领域来在 OAuth2 安全应用程序中实现多租户。我正在用 Go 创建一个原型,但并没有真正绑定到该语言,如果需要,可以切换到 Node.js 或 Java。我认为如果我切换语言,我的以下问题将成立。
起初,实施多租户对我来说似乎很简单:
- 为每个租户创建一个领域,其中包含我的后端应用程序所需的客户端配置。
- 后端收到带有 URL
tenant-1.my-app.com
的请求。解析 URL 以检索要用于身份验证的租户。 - 连接到 OAuth2 提供程序(在本例中为 Keycloak)并验证请求令牌。
根据指南,我使用 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
锁很重要。