使用AspNetCore.OData 7.2.1时GetRouteData始终为null [英] GetRouteData always null using AspNetCore.OData 7.2.1

查看:63
本文介绍了使用AspNetCore.OData 7.2.1时GetRouteData始终为null的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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

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

是否有替代方法?您可以在下面的代码中进行尝试.

Startup.cs

 公共启动(IConfiguration配置){配置=配置;}公共IConfiguration配置{}//此方法由运行时调用.使用此方法将服务添加到容器.公共无效ConfigureServices(IServiceCollection服务){services.AddHttpContextAccessor();services.AddAuthentication("BasicAuthentication").AddScheme< AuthenticationSchemeOptions,BasicAuthenticationHandler>("BasicAuthentication",null);services.AddMvc(options => options.EnableEndpointRouting = false).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);services.AddOData();}//此方法由运行时调用.使用此方法来配置HTTP请求管道.公共无效配置(IApplicationBuilder应用程序,IHostingEnvironment env){如果(env.IsDevelopment()){app.UseDeveloperExceptionPage();}//需要能够通过IHttpContextAccessor从HttpContext获取RouteDataapp.UseEndpointRouting();//需要使用标准的Authorize属性保护应用程序的安全app.UseAuthentication();//OData实体模型构建器var builder = new ODataConventionModelBuilder(app.ApplicationServices);builder.EntitySet< Value>(nameof(Value)+"s");app.UseMvc();app.UseOData("odata","{tenant}/odata",builder.GetEdmModel());//受到相同问题影响的替代配置////app.UseMvc(routeBuilder =>//{////映射OData路由,为基于租户的url添加令牌//routeBuilder.MapODataServiceRoute("odata","{tenant}/odata",builder.GetEdmModel());//////需要允许注入OData类//routeBuilder.EnableDependencyInjection();//});} 

BasicAuthenticationHandler.cs

 公共类BasicAuthenticationHandler:AuthenticationHandler< AuthenticationSchemeOptions>{私有只读IHttpContextAccessor _httpContextAccessor;公共BasicAuthenticationHandler(IOptionsMonitor< AuthenticationSchemeOptions>选项,ILoggerFactory记录器,UrlEncoder编码器,ISystemClock时钟,IHttpContextAccessor httpContextAccessor):基本(选项,记录器,编码器,时钟){_httpContextAccessor = httpContextAccessor;}公共字符串GetTenant(){var httpContext = _httpContextAccessor?.HttpContext;var routeData = httpContext?.GetRouteData();//此结果始终为空路由数据!返回routeData?.Values ["tenant"] ?. ToString();}受保护的覆盖异步任务< AuthenticateResult>HandleAuthenticateAsync(){如果(!Request.Headers.ContainsKey("Authorization"))返回AuthenticateResult.Fail(缺少授权头");尝试 {var authHeader = AuthenticationHeaderValue.Parse(Request.Headers ["Authorization"]);var credentialBytes = Convert.FromBase64String(authHeader.Parameter);var凭证= Encoding.UTF8.GetString(credentialBytes).Split(':');var用户名=凭据[0];var password =凭据[1];var tenant = GetTenant();如果(string.IsNullOrEmpty(tenant)){返回AuthenticateResult.Fail(未知租户");}if(string.IsNullOrEmpty(username)||用户名!=密码)返回AuthenticateResult.Fail(用户名或密码错误");}捕获(异常e){返回AuthenticateResult.Fail(无法验证");}var Claims = new [] {new Claim("Tenant","tenant id")};var identity = new ClaimsIdentity(claims,Scheme.Name);varPrincipal = new ClaimsPrincipal(identity);var ticket = new AuthenticationTicket(principal,Scheme.Name);返回AuthenticateResult.Success(ticket);}受保护的重写异步任务HandleChallengeAsync(AuthenticationProperties属性){Response.Headers ["WWW-Authenticate"] =基本领域= \"哦,我的OData \,字符集= \" UTF-8 \";等待base.HandleChallengeAsync(properties);}} 

Value.cs

 公共类值{public int ID {get;放;}公共字符串名称{get;放;}} 

ValuesController.cs

  [授权]公共类ValuesController:ODataController{私有列表<值>_values;公共ValuesController(){_values = new List< Value>{新值{Id = 1,名称="A1"},新值{Id = 2,名称="A2"},新值{Id = 3,名称="A3"},新值{Id = 4,名称="A4"},新值{Id = 5,名称="A5"},新值{Id = 6,名称="A6"},新值{Id = 7,名称="A7"},新值{Id = 11,名称="B1"},新值{Id = 12,名称="B2"},新值{Id = 13,名称="B3"},新值{Id = 14,名称="B4"},新值{Id = 15,名称="B5"},新值{Id = 16,名称="B6"},新值{Id = 17,名称="B7"}};}//GET {租户}/odata/values[EnableQuery]公共IQueryable< Value>得到(){返回_values.AsQueryable();}//GET {tenant}/odata/values/5[EnableQuery]public ActionResult< Value>Get([FromODataUri] int键){if(_values.Any(v => v.Id ==键))return _values.Single(v => v.Id ==键);返回NotFound();}} 

在工作的应用程序中添加了示例代码以重现该问题并测试解决方案:

I am trying to secure an OData api using .net Core 2.2 and AspNetCore.OData 7.2.1, with a basic authentication handler. I need to handler multi tenant urls, and retrieve from the uri the token that will then be used in the authorization handler to determine if a user is authorized.

To do so I use the IHttpContextAccessor but this works only with standard api, and not with OData.

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?

Is there an alternative approach? Below the code you can use to try this out.

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: Added sample code in working application to reproduce the issue and test solutions: 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?

As you know, OData doesn't work fine with ASP.NET Core 2.2 EndPoint Routing. For more details at present, see https://github.com/OData/WebApi/issues/1707

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

The reason why you always get a null route data is that the Authentication middleware runs before the Router middleware takes effect. In other words, you won't get route data before the Router middleware is invoked.

To walk around it, just create a router and make it runs before the Authentication middleware.

How to fix

  1. Make sure you've disabled the EnableEndpointRouting:

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

  2. Remove the line of 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. Set up a Router before Authentication so that you can get the Route Data within AuthenticationHandler later:

    // 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?}");
    });
    

Demo

  1. Send a request to the Values API

    GET https://localhost:5001/msft/odata/values
    Authorization: Basic dGVzdDp0ZXN0
    

  2. And then we'll get the route data as below:

这篇关于使用AspNetCore.OData 7.2.1时GetRouteData始终为null的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆