在 .Net Core 3.1 中更改请求路径

Changing Request Path in .Net Core 3.1

在 3.0 之前,我可以通过访问 HttpContextHttpRequest 属性 然后更改请求的路径(无需任何形式的浏览器重定向) Path.

的值

例如,要为需要更改 his/her 密码的用户显示一个页面(无论用户打算访问哪个页面),我扩展了 HttpContext

public static void ChangeDefaultPassword(this HttpContext context) 
=> context.Request.Path = "/Account/ChangePassword";

这段代码将用户带到 AccountController 中的操作方法 ChangePassword 而没有执行 用户打算访问的操作方法。

然后进入dotnet core 3.1.

在3.1中,扩展方法改变了路径。但是,它从不执行操作方法。它忽略更新的路径。

我知道这是由于 routing.The 端点的更改现在可以使用扩展方法 HttpContext.GetEndpoint() 访问。还有一个扩展方法 HttpContext.SetEndpoint 似乎是设置新端点的正确方法。但是,没有关于如何完成此操作的示例。

问题

如何更改请求路径,而不执行原始路径?

我试过的

  1. 我试过改变路径。 dotnet core 3.1 中的路由似乎忽略了 HttpRequest 路径值的值。
  2. 我尝试使用 context.Response.Redirect("/Account/ChangePassword"); 进行重定向。这有效,但它 first 执行了用户请求的原始操作方法。这种行为违背了目的。
  3. 我尝试使用扩展方法 HttpContext.SetEndpoint,但没有可用的示例。

我遇到了类似的重新路由问题。就我而言,我想在 AuthorationHandler 失败时将用户重新路由到 "you don't have permissions" 视图。我应用了以下代码,特别是 (.Net Core 3.1) 中的 (httpContext.Response.Redirect(...)) 将我路由到家庭控制器上的 NoPermissions 操作。

在处理程序中 class:

 protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, FooBarRequirement requirement) {
var hasAccess = await requirement.CheckAccess(context.User);
if (hasAccess)
context.Succeed(requirement);
else {
var message = "You do not have access to this Foobar function";
AuthorizeHandler.NoPermission(mHttpContextAccessor.HttpContext, context, requirement, message);
 }
}

我写了一个静态的class来处理重定向,传入controller和action期望的url加上错误信息,重定向永久标志设置为true:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;

namespace Foo.BusinessLogic.Security {
public static class AuthorizeHandler {
    public static void NoPermission(HttpContext httpContext, 
AuthorizationHandlerContext context, IAuthorizationRequirement requirement, string 
errorMessage) {
        context.Succeed(requirement);
        httpContext.Response.Redirect($"/home/nopermission/?m={errorMessage}", true);
    }
  }
}

最后是处理视图和消息的控制器和动作

[AllowAnonymous]
public IActionResult NoPermission(string m) {
 return View("NoPermission", m);
 }
}

我找到了可行的解决方案。我的解决方案通过使用 SetEndpoint 扩展方法手动设置新端点来工作。

这是我创建的用于解决此问题的扩展方法。

    private static void RedirectToPath(this HttpContext context, string controllerName, string actionName )
    {
        // Get the old endpoint to extract the RequestDelegate
        var currentEndpoint = context.GetEndpoint();

        // Get access to the action descriptor collection
        var actionDescriptorsProvider =
            context.RequestServices.GetRequiredService<IActionDescriptorCollectionProvider>();

        // Get the controller aqction with the action name and the controller name.
        // You should be redirecting to a GET action method anyways. Anyone can provide a better way of achieving this. 
        var controllerActionDescriptor = actionDescriptorsProvider.ActionDescriptors.Items
            .Where(s => s is ControllerActionDescriptor bb
                        && bb.ActionName == actionName
                        && bb.ControllerName == controllerName
                        && (bb.ActionConstraints == null
                            || (bb.ActionConstraints != null
                               && bb.ActionConstraints.Any(x => x is HttpMethodActionConstraint cc
                               && cc.HttpMethods.Contains(HttpMethods.Get)))))
            .Select(s => s as ControllerActionDescriptor)
            .FirstOrDefault();

        if (controllerActionDescriptor is null) throw new Exception($"You were supposed to be redirected to {actionName} but the action descriptor could not be found.");

        // Create a new route endpoint
        // The route pattern is not needed but MUST be present. 
        var routeEndpoint = new RouteEndpoint(currentEndpoint.RequestDelegate, RoutePatternFactory.Parse(""), 1, new EndpointMetadataCollection(new object[] { controllerActionDescriptor }), controllerActionDescriptor.DisplayName);

        // set the new endpoint. You are assured that the previous endpoint will never execute.
        context.SetEndpoint(routeEndpoint);
    }

重要

  1. 您必须将操作方法​​的视图放在 Shared 文件夹中,以使其可用。或者,您可以决定提供 IViewLocationExpander
  2. 的自定义实现
  3. 在访问端点之前,路由中间件必须已经执行。

用法

public static void ChangeDefaultPassword(this HttpContext context) 
=> context.RedirectToPath("Account","ChangePassword");

在我的例子中,我在 DynamicRouteValueTransformer 中手动选择匹配端点。我有一个主要工作的解决方案,但必须切换到其他优先事项。也许其他人可以使用内置的 Action 执行器创建更优雅的解决方案。

RequestDelegate requestDelegate = async (HttpContext x) =>
{//manually handle controller activation, method invocation, and result processing
    var actionContext = new ActionContext(x, new RouteData(values), new ControllerActionDescriptor() { ControllerTypeInfo = controllerType.GetTypeInfo() });
    var activator = x.RequestServices.GetService(typeof(IControllerActivator)) as ServiceBasedControllerActivator;
    var controller = activator.Create(new ControllerContext(actionContext));
    var arguments = methodInfo.GetParameters().Select(p =>
    {
        object r;
        if (requestData.TryGetValue(p.Name, out object value)) r = value;
        else if (p.ParameterType.IsValueType) r = Activator.CreateInstance(p.ParameterType);
        else r = null;
        return r;
    });
    var actionResultTask = methodInfo.Invoke(controller, arguments.ToArray());
    var actionTask = actionResultTask as Task<IActionResult>;
    if (actionTask != null)
    {
        var actionResult = await actionTask;
        await actionResult.ExecuteResultAsync(actionContext);//errors here. actionContext is incomplete
    }
};

var endpoint = new Endpoint(requestDelegate, EndpointMetadataCollection.Empty, methodInfo.Name);
httpContext.SetEndpoint(endpoint);

我解决这个问题的方法是直接使用 EndpointDataSource,这是一个单例服务,只要您注册了路由服务就可以从 DI 获得。只要您可以提供可以在编译时指定的控制器名称和操作名称,它就可以工作。这不需要使用 IActionDescriptorCollectionProvider 或自己构建端点对象或请求委托(这非常复杂......):

public static void RerouteToActionMethod(this HttpContext context, EndpointDataSource endpointDataSource, string controllerName, string actionName)
{
    var endpoint = endpointDataSource.Endpoints.FirstOrDefault(e =>
    {
        var descriptor = e.Metadata.GetMetadata<ControllerActionDescriptor>();
        // you can add more constraints if you wish, e.g. based on HTTP method, etc
        return descriptor != null
               && actionName.Equals(descriptor.ActionName, StringComparison.OrdinalIgnoreCase)
               && controllerName.Equals(descriptor.ControllerName, StringComparison.OrdinalIgnoreCase);
    });

    if (endpoint == null)
    {
        throw new Exception("No valid endpoint found.");
    }

    context.SetEndpoint(endpoint);
}

检查您的中间件订单。

.UseRouting() 公开的中间件负责根据传入的请求路径决定命中哪个端点。如果您的路径重写中间件稍后出现在管道中(就像我的一样),那就太晚了,路由决策已经做出。

UseRouting() 之前移动我的自定义中间件可确保在命中路由中间件之前根据需要设置路径。

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, TelemetryConfiguration telemetryConfig)
{   
   //snip
   app.UseMiddleware<PathRewritingMiddleware>();
   app.UseRouting();
   app.UseEndpoints(endpoints =>
   {
     endpoints.MapControllers();
   });
   //snip       
}