我需要在asp.net核心上实现多租户REST API,并使用Jwt Web令牌进行身份验证。
Asp.net核心文档建议在Startup.cs ConfigureServices方法中使用以下代码:
services.AddAuthentication().AddJwtBearer("Bearer", options =>
{
options.Audience = "MyAudience";
options.Authority = "https://myauhorityserver.com";
}
问题是我的REST API应用程序是多租户。租户是从URL(例如
https://apple.myapi.com,
https://samsung.myapi.com,
https://google.myapi.com
因此,每个此类URL最终都将指向相同的IP,但是基于URL中的第一个单词,应用程序会在使用适当的db连接时发现租户。
每个这样的租户都有其自己的授权URL。我们将Keycloak用作身份管理服务器,因此其上的每个租户都有其自己的REALM。 因此,每个租户的授权URL是这样的:
https://mykeycloack.com/auth/realms/11111111,
https://mykeycloack.com/auth/realms/22222222,
https://mykeycloack.com/auth/realms/33333333
API应用程序应该能够动态添加和删除租户,而无需重新启动应用程序,因此,在应用程序启动时设置所有租户不是一个好主意。
我试图通过对AddJwtBearer的更多调用来添加更多模式,但是根据options.Events.OnAuthenticationFailed事件,所有调用都转到了模式“ Bearer”。目前尚不清楚如何使其他模式处理HTTP标头中带有Bearer令牌的调用。即使通过自定义中间件在某种程度上可行,正如我之前提到的,在应用启动时为承租人令牌身份验证提供特定于承租人的配置也不是解决方案,因为需要动态添加新承租人。
其他信息: 根据提琴手的说法,授权URL最终与
结合/.well-known/openid-configuration
,并在第一个请求到达标有
的API端点时调用[Authorize]
如果to配置失败,则API调用失败。如果对配置的调用成功,则应用程序不会在下一个API请求上再次调用它。
期待任何建议。预先感谢。
答案 0 :(得分:3)
我找到了一个解决方案,并使其成为一个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();
}
}
服务
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);
}
}
与原始版本有何不同
// before
if (_configuration == null && Options.ConfigurationManager != null)
{
_configuration = await
Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
}
// after
var currentConfiguration = await this.dynamicJwtBearerHanderConfigurationResolver.ResolveCurrentOpenIdConfiguration(Context);
其替换方式
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, _ => { });
}
处理程序的来源
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;
}
}
}
答案 1 :(得分:0)
如果您仍在尝试解决此问题,可以尝试Finbuckle。