在ASP.NET Core中的Controllers上下文外部重定向

时间:2018-07-19 08:12:02

标签: c# asp.net asp.net-core-mvc middleware

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

也许还有其他更好的模式(如果您知道,让我知道,我会查一下),但是我很好奇是否可行。

当您必须调用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调用。

现在我想知道的是,当我们将设置Authorization标头的逻辑或在控制器外部处理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);不起作用。它不会中断要重定向的流程。有可能实现它,您将如何解决?

1 个答案:

答案 0 :(得分:1)

好吧,因为没有人回答我的问题,我已经仔细考虑了一下,然后提出了以下建议:

设置

我们有一个资源网关(RG)。 RG可以返回401或403,表示会话已过期(401)或用户没有足够的权限(403)。我们使用访问令牌(AT)对向RG的请求进行身份验证和授权。

身份验证

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

授权

当我们收到403时,我们想向用户显示他无权访问或类似的内容。

解决方案

要处理上述问题,而又不会给使用API​​或API包装器类的程序员带来麻烦,我们可以使用中间件,该中间件将专门处理使用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用作静态方法的包装器类。

您可以这样做:

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>();

现在,您可以在所需的任何控制器中使用依赖项注入。

处理异常

创建类似此中间件的东西(感谢answer):

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参考。

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