使用 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
中间件之前运行。
如何修复
确保您已禁用 EnableEndpointRouting
:
services.AddMvc(
options => options.EnableEndpointRouting = false
)
.SetCompatibilityVersion(CompatibilityVersion.Version_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();
在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?}");
});
演示
向值API
发送请求
GET https://localhost:5001/msft/odata/values
Authorization: Basic dGVzdDp0ZXN0
然后我们得到的路由数据如下:
我正在尝试使用 .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
中间件之前运行。
如何修复
确保您已禁用
EnableEndpointRouting
:services.AddMvc( options => options.EnableEndpointRouting = false ) .SetCompatibilityVersion(CompatibilityVersion.Version_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();
在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?}"); });
演示
向值API
发送请求GET https://localhost:5001/msft/odata/values Authorization: Basic dGVzdDp0ZXN0
然后我们得到的路由数据如下: