ASP.NET Core MVC 2.0 中基于路径的身份验证

Path based authentication in ASP.NET Core MVC 2.0

在 ASP.NET Core MVC 1.1 中,我们有这样基于路径的身份验证:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    // /api/* path
    app.UseWhen(ctx => IsApiRequest(ctx), subBranch =>
    {
        subBranch.UseApiAuth(GetApiAuthOptions());
    });
    // else
    app.UseWhen(ctx => !IsApiRequest(ctx), subBranch =>
    {
        subBranch.UseOpenIdConnectAuthentication(GetOpenIdOptions());
    });
}

现在我们要将它迁移到 ASP.NET Core MVC 2.0。在新版本中,身份验证被完全重新设计,在文档中我没有找到任何线索如何做到这一点。 对如何迁移上面的代码有什么想法吗?

经过 2 天的测试和尝试,我想出了可行的解决方案。

主要问题是,在 ASP.NET Core MVC 2.0 中,身份验证方法被注册为服务而不是中间件。 这意味着它们必须在 ConfigureServices 方法中而不是在 Configure 中注册,因此无法在注册时创建分支。 此外,身份验证系统使用 AuthenticationOptions 来确定将使用哪种身份验证方法。 从我的测试中我发现,AuthenticationOptions 实例在请求之间共享,因此不能修改它来调整 DefaultScheme。 经过一些挖掘,我发现 IAuthenticationSchemeProvider,可以覆盖它来克服这些问题。

代码如下:

// Startup.cs
public IServiceProvider ConfigureServices(IServiceCollection services)
{
    [...]

    // Override default IAuthenticationSchemeProvider implementation
    services.AddSingleton<IAuthenticationSchemeProvider, CustomAuthenticationSchemeProvider>();

    // Register OpenId Authentication services
    services.AddAuthentication().AddCookie(this.GetCookieOptions);
    services.AddAuthentication().AddOpenIdConnect(this.GetOpenIdOptions);

    // Register HMac Authentication services (for API)
    services.AddAuthentication().AddHMac(this.GetHMacOptions);

    [...]
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    [...]

    // /api/* path
    app.UseWhen(ctx => IsApiRequest(ctx), subBranch =>
    {
        // Register middleware which will override DefaultScheme; must be called before UseAuthentication()
        subBranch.UseAuthenticationOverride(HMacAuthenticationDefaults.AuthenticationScheme);
        subBranch.UseAuthentication();
    });
    // else
    app.UseWhen(ctx => !IsApiRequest(ctx), subBranch =>
    {
        // Register middleware which will override DefaultScheme and DefaultChallengeScheme; must be called before UseAuthentication()
        subBranch.UseAuthenticationOverride(new AuthenticationOptions
        {
            DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme,
            DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme
        });
        subBranch.UseAuthentication();
    });

    [...]
}

中间件:

// AuthenticationOverrideMiddleware.cs
// Adds overriden AuthenticationOptions to HttpContext to be used by CustomAuthenticationSchemeProvider
public class AuthenticationOverrideMiddleware
{
    private readonly RequestDelegate _next;
    private readonly AuthenticationOptions _authenticationOptionsOverride;

    public AuthenticationOverrideMiddleware(RequestDelegate next, AuthenticationOptions authenticationOptionsOverride)
    {
        this._next = next;
        this._authenticationOptionsOverride = authenticationOptionsOverride;
    }
    public async Task Invoke(HttpContext context)
    {
        // Add overriden options to HttpContext
        context.Features.Set(this._authenticationOptionsOverride);
        await this._next(context);
    }
}
public static class AuthenticationOverrideMiddlewareExtensions
{
    public static IApplicationBuilder UseAuthenticationOverride(this IApplicationBuilder app, string defaultScheme)
    {
        return app.UseMiddleware<AuthenticationOverrideMiddleware>(new AuthenticationOptions { DefaultScheme = defaultScheme });
    }
    public static IApplicationBuilder UseAuthenticationOverride(this IApplicationBuilder app, AuthenticationOptions authenticationOptionsOverride)
    {
        return app.UseMiddleware<AuthenticationOverrideMiddleware>(authenticationOptionsOverride);
    }
}

CustomAuthenticationSchemeProvider:

// CustomAuthenticationSchemeProvider.cs
// When asked for Default*Scheme, will check in HttpContext for overriden options, and return appropriate schema name
public class CustomAuthenticationSchemeProvider : AuthenticationSchemeProvider
{
    private readonly IHttpContextAccessor _contextAccessor;

    public CustomAuthenticationSchemeProvider(IOptions<AuthenticationOptions> options, IHttpContextAccessor contextAccessor) : base(options)
    {
        this._contextAccessor = contextAccessor;
    }

    // Retrieves overridden options from HttpContext
    private AuthenticationOptions GetOverrideOptions()
    {
        HttpContext context = this._contextAccessor.HttpContext;
        return context?.Features.Get<AuthenticationOptions>();
    }
    public override Task<AuthenticationScheme> GetDefaultAuthenticateSchemeAsync()
    {
        AuthenticationOptions overrideOptions = this.GetOverrideOptions();
        string overridenScheme = overrideOptions?.DefaultAuthenticateScheme ?? overrideOptions?.DefaultScheme;
        if (overridenScheme != null)
            return this.GetSchemeAsync(overridenScheme);
        return base.GetDefaultAuthenticateSchemeAsync();
    }
    public override Task<AuthenticationScheme> GetDefaultChallengeSchemeAsync()
    {
        AuthenticationOptions overrideOptions = this.GetOverrideOptions();
        string overridenScheme = overrideOptions?.DefaultChallengeScheme ?? overrideOptions?.DefaultScheme;
        if (overridenScheme != null)
            return this.GetSchemeAsync(overridenScheme);
        return base.GetDefaultChallengeSchemeAsync();
    }
    public override Task<AuthenticationScheme> GetDefaultForbidSchemeAsync()
    {
        AuthenticationOptions overrideOptions = this.GetOverrideOptions();
        string overridenScheme = overrideOptions?.DefaultForbidScheme ?? overrideOptions?.DefaultScheme;
        if (overridenScheme != null)
            return this.GetSchemeAsync(overridenScheme);
        return base.GetDefaultForbidSchemeAsync();
    }
    public override Task<AuthenticationScheme> GetDefaultSignInSchemeAsync()
    {
        AuthenticationOptions overrideOptions = this.GetOverrideOptions();
        string overridenScheme = overrideOptions?.DefaultSignInScheme ?? overrideOptions?.DefaultScheme;
        if (overridenScheme != null)
            return this.GetSchemeAsync(overridenScheme);
        return base.GetDefaultSignInSchemeAsync();
    }
    public override Task<AuthenticationScheme> GetDefaultSignOutSchemeAsync()
    {
        AuthenticationOptions overrideOptions = this.GetOverrideOptions();
        string overridenScheme = overrideOptions?.DefaultSignOutScheme ?? overrideOptions?.DefaultScheme;
        if (overridenScheme != null)
            return this.GetSchemeAsync(overridenScheme);
        return base.GetDefaultSignOutSchemeAsync();
    }
}

如果有人知道更好的解决方案,我很乐意看到它。