GetRouteData всегда нулевой, используя AspNetCore.OData 7.2.1 - PullRequest
1 голос
/ 19 сентября 2019

Я пытаюсь защитить API OData, используя .net Core 2.2 и AspNetCore.OData 7.2.1, с базовым обработчиком аутентификации.Мне нужно обработать 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();
    }
}

EDIT: добавлен пример кода в рабочем приложении для воспроизведениярешения проблем и испытаний: https://github.com/norcino/so-58016881-OData-GetRoute

1 Ответ

1 голос
/ 23 сентября 2019

OData не нравится EndpointRouting, и мне пришлось отключить его, как показано ниже, но в таком случае как мне получить доступ к RouteData для получения токена арендатора?

Как вы знаете,OData не работает нормально с маршрутизацией конечной точки ASP.NET Core 2.2.Для получения более подробной информации см. https://github.com/OData/WebApi/issues/1707

var routeData = httpContext?.GetRouteData(); // ЭТО РЕЗУЛЬТАТЫ ВСЕГДА В ДАННЫХ МАРШРУТНЫХ ДАННЫХ!

Причина, по которой вы всегда получаете null данные маршрута - это то, что промежуточное ПО аутентификации запускается до того, как промежуточное ПО маршрутизатора вступит в силуДругими словами, вы не получите данные о маршруте, пока не будет запущено промежуточное программное обеспечение маршрутизатора .

Чтобы обойти его, просто создайте маршрутизатор и запустите его до промежуточного программного обеспечения 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. Настройте маршрутизатор до аутентификации, чтобы вы могли получить данные маршрута в течение AuthenticationHandler позже:

    // 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. И тогда мы получим данные маршрута в видениже:

    enter image description here

...