使用 AspNetCore.OData 7.2.1 GetRouteData 始终为 null

GetRouteData always null using AspNetCore.OData 7.2.1

我正在尝试使用 .net Core 2.2 和 AspNetCore.OData 7.2.1 以及基本身份验证处理程序来保护 OData api。 我需要处理多租户 url,并从 uri 中检索令牌,然后在授权处理程序中使用该令牌来确定用户是否已获得授权。

为此,我使用了 IHttpContextAccessor,但这仅适用于标准 api,不适用于 OData。

OData 不喜欢 EndpointRouting,我不得不如下所示禁用它,但在这种情况下,我如何访问 RouteData 以获取租户令牌?

是否有替代方法?您可以在代码下方尝试一下。

Startup.cs

public Startup(IConfiguration configuration)
{
    Configuration = configuration;
}

public IConfiguration Configuration { get; }

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpContextAccessor();
    services.AddAuthentication("BasicAuthentication")
        .AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("BasicAuthentication", null);

    services.AddMvc(options => options.EnableEndpointRouting = false)
        .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
    services.AddOData();
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    // Needed to be able to get RouteData from HttpContext through the IHttpContextAccessor
    app.UseEndpointRouting();
    // Needed to secure the application using the standard Authorize attribute
    app.UseAuthentication();

    // OData entity model builder
    var builder = new ODataConventionModelBuilder(app.ApplicationServices);
    builder.EntitySet<Value>(nameof(Value) + "s");

    app.UseMvc();
    app.UseOData("odata", "{tenant}/odata", builder.GetEdmModel());

// Alternative configuration which is affected by the same problem
//
//  app.UseMvc(routeBuilder =>
//  {
//      // Map OData routing adding token for the tenant based url
//      routeBuilder.MapODataServiceRoute("odata", "{tenant}/odata", builder.GetEdmModel());
//  
//      // Needed to allow the injection of OData classes
//      routeBuilder.EnableDependencyInjection();
//  });
}

BasicAuthenticationHandler.cs

public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public BasicAuthenticationHandler(
        IOptionsMonitor<AuthenticationSchemeOptions> options,
        ILoggerFactory logger,
        UrlEncoder encoder,
        ISystemClock clock,
        IHttpContextAccessor httpContextAccessor)
        : base(options, logger, encoder, clock)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public string GetTenant()
    {
        var httpContext = _httpContextAccessor?.HttpContext;
        var routeData = httpContext?.GetRouteData(); // THIS RESULTS ALWAYS IN NULL ROUTE DATA!
        return routeData?.Values["tenant"]?.ToString();
    }

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        if (!Request.Headers.ContainsKey("Authorization"))
            return AuthenticateResult.Fail("Missing Authorization Header");

        try {
            var authHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]);

            var credentialBytes = Convert.FromBase64String(authHeader.Parameter);
            var credentials = Encoding.UTF8.GetString(credentialBytes).Split(':');
            var username = credentials[0];
            var password = credentials[1];

            var tenant = GetTenant();

            if (string.IsNullOrEmpty(tenant))
            {
                return AuthenticateResult.Fail("Unknown tenant");
            }

            if(string.IsNullOrEmpty(username) || username != password)
                return AuthenticateResult.Fail("Wrong username or password");
    }
        catch (Exception e)
        {
            return AuthenticateResult.Fail("Unable to authenticate");
        }

        var claims = new[] {
            new Claim("Tenant", "tenant id")
        };

        var identity = new ClaimsIdentity(claims, Scheme.Name);
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, Scheme.Name);

        return AuthenticateResult.Success(ticket);
    }

    protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
    {
        Response.Headers["WWW-Authenticate"] = "Basic realm=\"Oh my OData\", charset=\"UTF-8\"";
        await base.HandleChallengeAsync(properties);
    }
}

Value.cs

public class Value
{
    public int Id { get; set; }
    public string Name { get; set; }
}

ValuesController.cs

[Authorize]
public class ValuesController : ODataController
{
    private List<Value> _values;

    public ValuesController()
    {
        _values = new List<Value>
        {
            new Value {Id = 1, Name = "A1"},
            new Value {Id = 2, Name = "A2"},
            new Value {Id = 3, Name = "A3"},
            new Value {Id = 4, Name = "A4"},
            new Value {Id = 5, Name = "A5"},
            new Value {Id = 6, Name = "A6"},
            new Value {Id = 7, Name = "A7"},
            new Value {Id = 11, Name = "B1"},
            new Value {Id = 12, Name = "B2"},
            new Value {Id = 13, Name = "B3"},
            new Value {Id = 14, Name = "B4"},
            new Value {Id = 15, Name = "B5"},
            new Value {Id = 16, Name = "B6"},
            new Value {Id = 17, Name = "B7"}
        };
    }

    // GET {tenant}/odata/values
    [EnableQuery]
    public IQueryable<Value> Get()
    {
        return _values.AsQueryable();
    }

    // GET {tenant}/odata/values/5
    [EnableQuery]
    public ActionResult<Value> Get([FromODataUri] int key)
    {
        if(_values.Any(v => v.Id == key))
            return _values.Single(v => v.Id == key);

        return NotFound();
    }
}

编辑: 在工作应用程序中添加示例代码以重现问题和测试解决方案:https://github.com/norcino/so-58016881-OData-GetRoute

OData does not like EndpointRouting and I had to disable it as shown below, but in this case then how can I access the RouteData to take the tenant token?

如您所知,OData 不能与 ASP.NET Core 2.2 端点路由一起正常工作。目前更多详情见https://github.com/OData/WebApi/issues/1707

var routeData = httpContext?.GetRouteData(); // THIS RESULTS ALWAYS IN NULL ROUTE DATA!

之所以总是得到一个null路由数据,是因为Authentication中间件运行在Router中间件生效之前。换句话说,你不会在调用 Router 中间件之前获取路由数据

要绕过它,只需创建一个路由器并使其在 Authentication 中间件之前运行。

如何修复

  1. 确保您已禁用 EnableEndpointRouting:

    services.AddMvc(
        options => options.EnableEndpointRouting = false
    )
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);   
    
  2. 删除app.UseEndpointRouting()行:

    // OData doesn't work fine with ASP.NET Core 2.2 EndPoint Routing, See https://github.com/OData/WebApi/issues/1707
    // app.UseEndpointRouting();  
    
  3. 在Authentication之前设置一个Router,以便稍后在AuthenticationHandler内获取Route Data:

    // configure Routes for OData
    app.UseRouter(routeBuilder =>{
        var templatePrefix="{tenant}/odata";
        var template = templatePrefix + "/{*any}";
        routeBuilder.MapMiddlewareRoute(template, appBuilder =>{
            var builder = new ODataConventionModelBuilder(app.ApplicationServices);
            builder.EntitySet<Value>(nameof(Value) + "s");
            appBuilder.UseAuthentication();
            appBuilder.UseMvc();
            appBuilder.UseOData("odata", templatePrefix, builder.GetEdmModel());
        });
    });
    
    // ... add more middlewares if you want other MVC routes
    app.UseAuthentication();
    app.UseMvc(rb => {
        rb.MapRoute("default","{controller=Home}/{action=Index}/{id?}");
    });
    

演示

  1. 向值API

    发送请求
    GET https://localhost:5001/msft/odata/values
    Authorization: Basic dGVzdDp0ZXN0
    
  2. 然后我们得到的路由数据如下: