Changes in Ocelot Middleware

Time:2021-9-10

Changes in Ocelot Middleware

Intro

When we used Ocelot, we customized some middleware to meet our customized needs. Recently, a little partner in the blog asked me how to use it. He used version 16.0. The difference between versions 16.0 and 17.0 is not very large. Let’s take version 17.0 as an example to see the changes of Ocelot middleware

Sample

Let’s take the previous middleware for user-defined authentication and authorization as an example. The middleware mainly does

  1. Query the required permissions based on resource (API path) and request method
  2. If you can access it without user login, you can forward it directly to the downstream service
  3. If permission is required, judge whether the role of the currently logged in user has a corresponding role to access
  4. If it can be accessed, it will be forwarded to the downstream service. If it does not have access permission, 403 Forbidden will be returned if the user is logged in, and 401 unauthorized will be returned if the user is not logged in

Before

For details of previous implementations (based on version 13. X), please refer to:https://www.cnblogs.com/weihanli/p/custom-authentication-authorization-in-ocelot.html

The approximate code is as follows:

public class UrlBasedAuthenticationMiddleware : Ocelot.Middleware.OcelotMiddleware
{
    private readonly IConfiguration _configuration;
    private readonly IMemoryCache _memoryCache;
    private readonly OcelotRequestDelegate _next;

    public UrlBasedAuthenticationMiddleware(OcelotRequestDelegate next, IConfiguration configuration, IMemoryCache memoryCache, IOcelotLoggerFactory loggerFactory) : base(loggerFactory.CreateLogger())
    {
        _next = next;

        _configuration = configuration;
        _memoryCache = memoryCache;
    }

    public async Task Invoke(DownstreamContext context)
    {
        var permissions = await _memoryCache.GetOrCreateAsync("ApiPermissions", async entry =>
                                                              {
                                                                  using (var conn = new SqlConnection(_configuration.GetConnectionString("ApiPermissions")))
                                                                  {
                                                                      entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1);
                                                                      return (await conn.QueryAsync("SELECT * FROM dbo.ApiPermissions")).ToArray();
                                                                  }
                                                              });

        var result = await context.HttpContext.AuthenticateAsync(context.DownstreamReRoute.AuthenticationOptions.AuthenticationProviderKey);
        context.HttpContext.User = result.Principal;

        var user = context.HttpContext.User;
        var request = context.HttpContext.Request;

        var permission = permissions.FirstOrDefault(p =>
                                                    request.Path.Value.Equals(p.PathPattern, StringComparison.OrdinalIgnoreCase) && p.Method.ToUpper() == request.Method.ToUpper());

        If (permission = = null) // if no match is found, match according to the regular
        {
            permission =
                permissions.FirstOrDefault(p =>
                                           Regex.IsMatch(request.Path.Value, p.PathPattern, RegexOptions.IgnoreCase) && p.Method.ToUpper() == request.Method.ToUpper());
        }

        if (!user.Identity.IsAuthenticated)
        {
            If (permission! = null & & string. Isnullorwhitespace (permission. Allowedroles)) // login is required by default
            {
                //context.HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, "Anonymous") }, context.DownstreamReRoute.AuthenticationOptions.AuthenticationProviderKey));
            }
            else
            {
                SetPipelineError(context, new UnauthenticatedError("unauthorized, need login"));
                return;
            }
        }
        else
        {
            if (!string.IsNullOrWhiteSpace(permission?.AllowedRoles) &&
                !permission.AllowedRoles.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Any(r => user.IsInRole(r)))
            {
                SetPipelineError(context, new UnauthorisedError("forbidden, have no permission"));
                return;
            }
        }

        await _next.Invoke(context);
    }
}

New

Let’s look at the implementation code in the new version of Ocelot (16. X / 17. X)

public class ApiPermission
{
    public string AllowedRoles { get; set; }

    public string PathPattern { get; set; }

    public string Method { get; set; }
}

public class UrlBasedAuthenticationMiddleware : Ocelot.Middleware.OcelotMiddleware
{
    private readonly IConfiguration _configuration;
    private readonly IMemoryCache _memoryCache;
    private readonly RequestDelegate _next;

    public UrlBasedAuthenticationMiddleware(RequestDelegate next, IConfiguration configuration, IMemoryCache memoryCache, IOcelotLoggerFactory loggerFactory) : base(loggerFactory.CreateLogger())
    {
        _next = next;

        _configuration = configuration;
        _memoryCache = memoryCache;
    }

    public async Task Invoke(HttpContext httpContext)
    {
        // var permissions = await _memoryCache.GetOrCreateAsync("ApiPermissions", async entry =>
        //{
        //    using (var conn = new SqlConnection(_configuration.GetConnectionString("ApiPermissions")))
        //    {
        //        entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1);
        //        return (await conn.QueryAsync("SELECT * FROM dbo.ApiPermissions")).ToArray();
        //    }
        //});

        var permissions = new[]
        {
           new ApiPermission()
           {
               PathPattern = "/api/test/values",
               Method = "GET",
               AllowedRoles = ""
           },
           new ApiPermission()
           {
               PathPattern = "/api/test/user",
               Method = "GET",
               AllowedRoles = "User"
           },
           new ApiPermission()
           {
               PathPattern = "/api/test/admin",
               Method = "GET",
               AllowedRoles = "Admin"
           },
        };

        var downstreamRoute = httpContext.Items.DownstreamRoute();

        var result = await httpContext.AuthenticateAsync(downstreamRoute.AuthenticationOptions.AuthenticationProviderKey);
        if (result.Principal != null)
        {
            httpContext.User = result.Principal;
        }

        var user = httpContext.User;
        var request = httpContext.Request;

        var permission = permissions.FirstOrDefault(p =>
            request.Path.ToString().Equals(p.PathPattern, StringComparison.OrdinalIgnoreCase) && p.Method.ToUpper() == request.Method.ToUpper());

        if (permission == null)
        {
            permission =
                permissions.FirstOrDefault(p =>
                    Regex.IsMatch(request.Path.ToString(), p.PathPattern, RegexOptions.IgnoreCase) && p.Method.ToUpper() == request.Method.ToUpper());
        }

        if (user.Identity?.IsAuthenticated == true)
        {
            if (!string.IsNullOrWhiteSpace(permission?.AllowedRoles) &&
                !permission.AllowedRoles.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
                    .Any(r => user.IsInRole(r)))
            {
                httpContext.Items.SetError(new UnauthorizedError("forbidden, have no permission"));
                return;
            }
        }
        else
        {
            if (permission != null && string.IsNullOrWhiteSpace(permission.AllowedRoles))
            {
            }
            else
            {
                httpContext.Items.SetError(new UnauthenticatedError("unauthorized, need login"));
                return;
            }
        }

        await _next.Invoke(httpContext);
    }
}

Diff

The main difference is the change of Ocelot middleware. In previous versions, Ocelot is its own middleware and the signature isTask Invoke(DownstreamContext context)It’s Ocelot’s ownDownstreamContextLater, in order to keep the same signature as the asp.net core middleware, Ocelot updated its middleware to better reuse the middleware in asp.net core. Ocelot’s own context and other information are now on the InternetHttpContext.ItemsAnd through a series of extension methods to obtain and update the corresponding information

However, the current implementation can not be completely equivalent to asp.net core middleware, because if you want to interrupt a middleware, there is probably a problem now, because there are many problems in Ocelot middlewareHttpContextIt’s not originalHttpContextOcelot creates a new one before it actually starts processing the requestHttpContextCopy the basic request information. The main implementation code is:https://github.com/ThreeMammals/Ocelot/blob/17.0.0/src/Ocelot/Multiplexer/MultiplexingMiddleware.cs

If you want to implement interrupt in custom middleware, you need to use Ocelot’s middleware to process it through seterror instead of directly using ithttpContext.ResponseDe interrupt request

API Diff

  1. middleware InvokeMethod signature, from the originalTask Invoke(DownstreamContext context)Update asTask Invoke(HttpContext context)
  2. SetPipelineErrorNo longerOcelotMiddlewareA method in, throughhttpContext.Items.SetErrorMethod instead
  3. adopthttpContext.Items.DownstreamRoute()To get the current requestDownstreamRouteinformation

More

In addition to the change of middleware, the configuration has also changedReRouteIt also becameRoute, you need to pay attention to the configuration changes when upgrading, otherwise it may 404. After 17.0,authorisationUpdated toauthorization, authoriseAlso updated toauthorize

For more updates, please refer to Ocelot’s PR changes and documentation

The examples mentioned in this article can get the complete code on GitHubhttps://github.com/WeihanLi/AspNetCorePlayground/tree/master/OcelotDemo

Reference