在多租户应用程序中添加JwtBearer

Yev*_*rko 6 authentication token jwt asp.net-core

我需要在 asp.net core 上实现多租户 REST API 并使用 Jwt Web 令牌进行身份验证。

Asp.net core 文档建议在 Startup.cs ConfigureServices 方法中使用以下代码:

    services.AddAuthentication().AddJwtBearer("Bearer", options =>
    {
        options.Audience = "MyAudience";
        options.Authority = "https://myauhorityserver.com";
    }
Run Code Online (Sandbox Code Playgroud)

问题是我的 REST API 应用程序是多租户的。租户是从 URL 发现的,例如

https://apple.myapi.com, 
https://samsung.myapi.com, 
https://google.myapi.com
Run Code Online (Sandbox Code Playgroud)

因此,每个此类 URL 最终都会指向相同的 IP,但根据 URL 中的第一个单词,应用程序会发现租户使用适当的数据库连接。

每个此类租户都有自己的授权 URL。我们使用Keycloak作为身份管理服务器,因此上面的每个租户都有自己的REALM。所以每个租户的授权 URL 是这样的:

https://mykeycloack.com/auth/realms/11111111, 
https://mykeycloack.com/auth/realms/22222222,
https://mykeycloack.com/auth/realms/33333333
Run Code Online (Sandbox Code Playgroud)

API 应用程序应该能够动态添加和删除租户,而无需重新启动应用程序,因此,在应用程序启动时设置所有租户并不是一个好主意。

我试图通过对 AddJwtBearer 的更多调用来添加更多架构,但是,根据 options.Events.OnAuthenticationFailed 事件,所有调用都会转到架构“Bearer”。目前尚不清楚如何使其他架构来处理 HTTP 标头中带有不记名令牌的调用。即使在自定义中间件的帮助下以某种方式可能,正如我之前提到的,在应用程序启动中为承载令牌身份验证提供特定于租户的配置也不是一个解决方案,因为需要动态添加新租户。

附加信息:根据 fiddler 的说法,Authority URL 最终与

/.well-known/openid-configuration
Run Code Online (Sandbox Code Playgroud)

并在第一个请求到达标记为的 API 端点时调用

[Authorize]
Run Code Online (Sandbox Code Playgroud)

如果 to 配置失败,API 调用也会失败。如果对配置的调用成功,应用程序不会在下一个 API 请求时再次调用它。

Dav*_*bee 7

我找到了一个解决方案,并将其制作为 Nuget 包以实现可重用,您可以看一下。

我几乎用 DynamicBearerTokenHandler 替换了 JwtBearerTokenHandler,它提供了在运行时解析 OpenIdConnectOptions 的灵活性。

我还制作了另一个包来为您处理这个问题,您唯一需要的就是解析正在进行的请求的权限,您会收到 HttpContext。

这是包: https: //github.com/PoweredSoft/DynamicJwtBearer

如何使用它。

public class Startup
{
public void ConfigureServices(IServiceCollection services)
        {
            services.AddHttpContextAccessor();
            services.AddMemoryCache();
            services
                .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddDynamicJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
                {
                    options.TokenValidationParameters.ValidateAudience = false;
                })
                .AddDynamicAuthorityJwtBearerResolver<ResolveAuthorityService>();

            services.AddControllers();
        }
}
Run Code Online (Sandbox Code Playgroud)

服务

internal class ResolveAuthorityService : IDynamicJwtBearerAuthorityResolver
    {
        private readonly IConfiguration configuration;

        public ResolveAuthorityService(IConfiguration configuration)
        {
            this.configuration = configuration;
        }

        public TimeSpan ExpirationOfConfiguration => TimeSpan.FromHours(1);

        public Task<string> ResolveAuthority(HttpContext httpContext)
        {
            var realm = httpContext.Request.Headers["X-Tenant"].FirstOrDefault() ?? configuration["KeyCloak:MasterRealm"];
            var authority = $"{configuration["KeyCloak:Endpoint"]}/realms/{realm}";
            return Task.FromResult(authority);
        }
    }
Run Code Online (Sandbox Code Playgroud)

和原来的有什么不同


// before 

if (_configuration == null && Options.ConfigurationManager != null)
{
    _configuration = await 
    Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
}

// after
var currentConfiguration = await this.dynamicJwtBearerHanderConfigurationResolver.ResolveCurrentOpenIdConfiguration(Context);
Run Code Online (Sandbox Code Playgroud)

它是如何被取代的

 public static AuthenticationBuilder AddDynamicJwtBearer(this AuthenticationBuilder builder, string authenticationScheme, Action<JwtBearerOptions> action = null)
        {
            builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<JwtBearerOptions>, JwtBearerPostConfigureOptions>());

            if (action != null)
                return builder.AddScheme<JwtBearerOptions, DynamicJwtBearerHandler>(authenticationScheme, null, action);

            return builder.AddScheme<JwtBearerOptions, DynamicJwtBearerHandler>(authenticationScheme, null, _ => { });
        }
Run Code Online (Sandbox Code Playgroud)

处理程序的来源

public class DynamicJwtBearerHandler : JwtBearerHandler
    {
        private readonly IDynamicJwtBearerHanderConfigurationResolver dynamicJwtBearerHanderConfigurationResolver;

        public DynamicJwtBearerHandler(IOptionsMonitor<JwtBearerOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IDynamicJwtBearerHanderConfigurationResolver dynamicJwtBearerHanderConfigurationResolver) : base(options, logger, encoder, clock)
        {
            this.dynamicJwtBearerHanderConfigurationResolver = dynamicJwtBearerHanderConfigurationResolver;
        }

        /// <summary>
        /// Searches the 'Authorization' header for a 'Bearer' token. If the 'Bearer' token is found, it is validated using <see cref="TokenValidationParameters"/> set in the options.
        /// </summary>
        /// <returns></returns>
        protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            string token = null;
            try
            {
                // Give application opportunity to find from a different location, adjust, or reject token
                var messageReceivedContext = new MessageReceivedContext(Context, Scheme, Options);

                // event can set the token
                await Events.MessageReceived(messageReceivedContext);
                if (messageReceivedContext.Result != null)
                {
                    return messageReceivedContext.Result;
                }

                // If application retrieved token from somewhere else, use that.
                token = messageReceivedContext.Token;

                if (string.IsNullOrEmpty(token))
                {
                    string authorization = Request.Headers[HeaderNames.Authorization];

                    // If no authorization header found, nothing to process further
                    if (string.IsNullOrEmpty(authorization))
                    {
                        return AuthenticateResult.NoResult();
                    }

                    if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
                    {
                        token = authorization.Substring("Bearer ".Length).Trim();
                    }

                    // If no token found, no further work possible
                    if (string.IsNullOrEmpty(token))
                    {
                        return AuthenticateResult.NoResult();
                    }
                }

                var currentConfiguration = await this.dynamicJwtBearerHanderConfigurationResolver.ResolveCurrentOpenIdConfiguration(Context);
                var validationParameters = Options.TokenValidationParameters.Clone();
                if (currentConfiguration != null)
                {
                    var issuers = new[] { currentConfiguration.Issuer };
                    validationParameters.ValidIssuers = validationParameters.ValidIssuers?.Concat(issuers) ?? issuers;

                    validationParameters.IssuerSigningKeys = validationParameters.IssuerSigningKeys?.Concat(currentConfiguration.SigningKeys)
                        ?? currentConfiguration.SigningKeys;
                }

                List<Exception> validationFailures = null;
                SecurityToken validatedToken;
                foreach (var validator in Options.SecurityTokenValidators)
                {
                    if (validator.CanReadToken(token))
                    {
                        ClaimsPrincipal principal;
                        try
                        {
                            principal = validator.ValidateToken(token, validationParameters, out validatedToken);
                        }
                        catch (Exception ex)
                        {
                            Logger.TokenValidationFailed(ex);

                            // Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the event.
                            if (Options.RefreshOnIssuerKeyNotFound && Options.ConfigurationManager != null
                                && ex is SecurityTokenSignatureKeyNotFoundException)
                            {
                                Options.ConfigurationManager.RequestRefresh();
                            }

                            if (validationFailures == null)
                            {
                                validationFailures = new List<Exception>(1);
                            }
                            validationFailures.Add(ex);
                            continue;
                        }

                        Logger.TokenValidationSucceeded();

                        var tokenValidatedContext = new TokenValidatedContext(Context, Scheme, Options)
                        {
                            Principal = principal,
                            SecurityToken = validatedToken
                        };

                        await Events.TokenValidated(tokenValidatedContext);
                        if (tokenValidatedContext.Result != null)
                        {
                            return tokenValidatedContext.Result;
                        }

                        if (Options.SaveToken)
                        {
                            tokenValidatedContext.Properties.StoreTokens(new[]
                            {
                                new AuthenticationToken { Name = "access_token", Value = token }
                            });
                        }

                        tokenValidatedContext.Success();
                        return tokenValidatedContext.Result;
                    }
                }

                if (validationFailures != null)
                {
                    var authenticationFailedContext = new AuthenticationFailedContext(Context, Scheme, Options)
                    {
                        Exception = (validationFailures.Count == 1) ? validationFailures[0] : new AggregateException(validationFailures)
                    };

                    await Events.AuthenticationFailed(authenticationFailedContext);
                    if (authenticationFailedContext.Result != null)
                    {
                        return authenticationFailedContext.Result;
                    }

                    return AuthenticateResult.Fail(authenticationFailedContext.Exception);
                }

                return AuthenticateResult.Fail("No SecurityTokenValidator available for token: " + token ?? "[null]");
            }
            catch (Exception ex)
            {
                Logger.ErrorProcessingMessage(ex);

                var authenticationFailedContext = new AuthenticationFailedContext(Context, Scheme, Options)
                {
                    Exception = ex
                };

                await Events.AuthenticationFailed(authenticationFailedContext);
                if (authenticationFailedContext.Result != null)
                {
                    return authenticationFailedContext.Result;
                }

                throw;
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)


小智 0

如果您仍然想弄清楚这一点,您可以尝试 Finbuckle。

https://www.finbuckle.com/MultiTenant/Docs/Authentication