Redirect outside of the Controllers context in ASP.NET Core

做~自己de王妃 提交于 2021-02-19 16:43:30

问题


I do not know if this is actually possible, but I think it' s worth a try to find out.

There are maybe other and better patterns (if you know one let me know, I will look them up) to do this, but I'm just curious to know if this is possible.

When you have to call an API you could do it directly from within the controller using the HttpClient like this:

    [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);
    }

When you would do this you'll have to reuse some code over and over again. You could just make a Repository but for some scenarios that would be overkill and you just want to make some quick and dirty API calls.

Now what I want to know is, when we move the logic of setting an Authorization header or handling the 401 and 403 responses outside the controller, how do you redirect or control the controller's action.

Lets say I create a Middleware for the HttpClient like this:

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;
    }

}

We can just do the request like this:

    [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);
    }

The problem here is that context.Response.Redirect(rederectUrl); does not work. It does not break off the flow to redirect. Is it possible to implement this, and how would you solve this?


回答1:


Ok since nobody answers my question I've thought about it thoroughly and I came up with the following:

Setup

We have a resource gateway (RG). The RG can return a 401 or 403 meaning that the session is expired (401) or the user does not have sufficient rights (403). We use an access token (AT) to authenticate and authorize our requests to the RG.

authentication

When we get a 401 and we have a refresh token (RT) we want to trigger something that will retrieve a new AT. When there is no RT or the RT is expired we want to reauthenticate the user.

authorization

When we get a 403 we want to show the user that he has no access or something similar like that.

Solution

To handle the above, without making it a hassle for the programmer that uses the API or API wrapper class we can use a Middleware that will specifically handle the Exception thrown by using the API or the API wrapper. The middleware can handle any of the above situations.

Create custom Exceptions

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

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

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

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

Throw exceptions

Create a wrapper or use the HttpClient middleware to manage the exception throwing.

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;
    }

}

Now you have to setup the HttpClient inside your Startup.cs. There are multiple ways to do this. I advise to use AddTransient to innitiate a wrapper class that uses a HttpClient as a static.

You could do it like this:

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";
        }
    }
}

And in your Startup.cs inside the ConfigureServices(IServiceCollection services) you can do:

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

Now you can use the dependency injection in any controller you would like.

Handle the Exceptions

Create something like this middleware (with thanks to this 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
        }
    }

Register your middleware

Go to Startup.cs and go to the Configure(IApplicationBuilder app, IHostingEnvironment env) method and add app.UseMiddleware<ApiErrorMiddleWare>();.

This should do it. Currently, I'm creating an example when it is publicly available (after peer review) I'll add a github reference.

I would like to hear some feedback on this solution or an alternative approach.



来源:https://stackoverflow.com/questions/51417586/redirect-outside-of-the-controllers-context-in-asp-net-core

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!