在 ASP.NET 核心中的控制器上下文之外重定向

Redirect outside of the Controllers context in ASP.NET Core

我不知道这是否真的可行,但我认为值得一试。

也许还有其他更好的模式(如果您知道请告诉我,我会查找它们)来执行此操作,但我只是想知道这是否可行。

当您必须调用 API 时,您可以使用 HttpClient 直接从控制器内部调用,如下所示:

    [Authorize]
    public async Task<IActionResult> Private()
    {
        //Example: get some access token to use in api call
        var accessToken = await HttpContext.GetTokenAsync("access_token");

        //Example: do an API call direcly using a static HttpClient wrapt in a service
        var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/api/some/endpoint");
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
        var response = await _client.Client.SendAsync(request);

        if (response.StatusCode == HttpStatusCode.Unauthorized)
        {
            //Handle situation where user is not authenticated
            var rederectUrl = "/account/login?returnUrl="+Request.Path;
            return Redirect(rederectUrl);
        }

        if (response.StatusCode == HttpStatusCode.Forbidden)
        {
            //Handle situation where user is not authorized
            return null;
        }

        var text = await response.Content.ReadAsStringAsync();

        Result result = JObject.Parse(text).ToObject<Result>();

        return View(result);
    }

当您这样做时,您将不得不一遍又一遍地重用一些代码。你可以只创建一个存储库,但对于某些情况来说,这会有点矫枉过正,你只想进行一些快速而肮脏的 API 调用。

现在我想知道的是,当我们将设置授权header或处理控制器外的401和403响应的逻辑移动时,您如何重定向或控制控制器的动作。

假设我像这样为 HttpClient 创建了一个中间件:

public class ResourceGatewayMessageHandler : HttpClientHandler
{
    private readonly IHttpContextAccessor _contextAccessor;

    public ResourceGatewayMessageHandler(IHttpContextAccessor context)
    {
        _contextAccessor = context;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        //Retrieve acces token from token store
        var accessToken = await _contextAccessor.HttpContext.GetTokenAsync("access_token");

        //Add token to request
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

        //Execute request
        var response = await base.SendAsync(request, cancellationToken);

        //When 401 user is probably not logged in any more -> redirect to login screen
        if (response.StatusCode == HttpStatusCode.Unauthorized)
        {
            //Handle situation where user is not authenticated
            var context = _contextAccessor.HttpContext;
            var rederectUrl = "/account/login?returnUrl="+context.Request.Path;
            context.Response.Redirect(rederectUrl); //not working
        }

        //When 403 user probably does not have authorization to use endpoint
        if (response.StatusCode == HttpStatusCode.Forbidden)
        {
            //Handle situation where user is not authorized
        }

        return response;
    }

}

我们可以这样请求:

    [Authorize]
    public async Task<IActionResult> Private()
    {
        //Example: do an API call direcly using a static HttpClient initiated with Middleware wrapt in a service
        var response = await _client.Client.GetAsync("https://example.com/api/some/endpoint");

        var text = await response.Content.ReadAsStringAsync();

        Result result = JObject.Parse(text).ToObject<Result>();

        return View(result);
    }

这里的问题是 context.Response.Redirect(rederectUrl); 不起作用。它不会中断要重定向的流程。是否可以实现这个,你会如何解决这个问题?

好的,因为没有人回答我的问题,所以我仔细考虑了一下,然后得出以下结论:

设置

我们有一个资源网关 (RG)。 RG 可以 return 401 或 403,这意味着会话已过期 (401) 或用户没有足够的权限 (403)。我们使用访问令牌 (AT) 来验证和授权我们对 RG 的请求。

身份验证

当我们收到 401 并且我们有一个刷新令牌 (RT) 时,我们想要触发一些东西来检索新的 AT。当没有 RT 或 RT 过期时,我们要重新验证用户。

授权

当我们收到 403 时,我们想向用户表明他没有访问权限或类似的东西。

解决方案

为了处理上述问题,在不给使用 API 或 API 包装器 class 的程序员带来麻烦的情况下,我们可以使用一个中间件来专门处理抛出的异常通过使用 API 或 API 包装器。中间件可以处理以上任何一种情况。

创建自定义例外

public class ApiAuthenticationException : Exception
{
    public ApiAuthenticationException()
    {
    }

    public ApiAuthenticationException(string message) : base(message)
    {
    }
}

public class ApiAuthorizationException : Exception
{
    public ApiAuthorizationException()
    {
    }

    public ApiAuthorizationException(string message) : base(message)
    {
    }
}

抛出异常

创建包装器或使用 HttpClient 中间件来管理异常抛出。

public class ResourceGatewayMessageHandler : HttpClientHandler
{
    private readonly IHttpContextAccessor _contextAccessor;

    public ResourceGatewayMessageHandler(IHttpContextAccessor context)
    {
        _contextAccessor = context;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        //Retrieve acces token from token store
        var accessToken = await _contextAccessor.HttpContext.GetTokenAsync("access_token");

        //Add token to request
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

        //Execute request
        var response = await base.SendAsync(request, cancellationToken);

        //When 401 user is probably not logged in any more -> redirect to login screen
        if (response.StatusCode == HttpStatusCode.Unauthorized)
        {
            throw new ApiAuthenticationException();
        }

        //When 403 user probably does not have authorization to use endpoint -> show error page
        if (response.StatusCode == HttpStatusCode.Forbidden)
        {
            throw new ApiAuthorizationException();
        }

        return response;
    }

}

现在您必须在 Startup.cs 中设置 HttpClient。有多种方法可以做到这一点。我建议使用 AddTransient 来启动一个使用 HttpClient 作为静态的包装器 class。

你可以这样做:

public class ResourceGatewayClient : IApiClient
{
    private static HttpClient _client;
    public HttpClient Client => _client;

    public ResourceGatewayClient(IHttpContextAccessor contextAccessor)
    {
        if (_client == null)
        {
            _client = new HttpClient(new ResourceGatewayMessageHandler(contextAccessor));
            //configurate default base address
            _client.BaseAddress = "https://gateway.domain.com/api";
        }
    }
}

而在你的Startup.cs里面ConfigureServices(IServiceCollection services)你可以做:

 services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
 services.AddTransient<ResourceGatewayClient>();

现在您可以在任何控制器中使用依赖注入。

处理异常

创建类似这个中间件的东西(感谢这个):

public class ApiErrorMiddleWare
{
    private readonly RequestDelegate next;

    public ApiErrorMiddleWare(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await next(context);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(context, ex);
        }
    }

    private async Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        if (exception is ApiAuthenticationException)
        {
            context.Response.Redirect("/account/login");
        }

        if (exception is ApiAuthorizationException)
        {
            //handle not authorized
        }
    }

注册你的中间件

转到Startup.cs并转到Configure(IApplicationBuilder app, IHostingEnvironment env)方法并添加app.UseMiddleware<ApiErrorMiddleWare>();

这应该可以做到。目前,我正在创建一个公开可用的示例(经过同行评审后),我将添加一个 github 参考。

我想听听有关此解决方案或替代方法的一些反馈。