ocelot 中间件的变化
Intro
之前我们使用 ocelot 的时候自定义了一些中间件来实现我们定制化的一些需求,最近博客园上有小伙伴问我怎么使用,他用的版本是 16.0 版本,16.0 和 17.0 版本的差异不是特别大,就以 17.0 版本为例看一下 ocelot 中间件的变化
Sample
还是拿之前的一个自定义认证授权的一个中间件为例,中间件做的事情主要是
基于 Resource(API Path) 以及 请求 Method 查询需要的权限
如果不需要用户登录就可以访问,就直接往下游服务转发
如果需要权限,判断当前登录用户的角色是否有对应的角色可以访问
如果可以访问就转发到下游服务,如果没有权限访问根据用户是否登录,已登录返回 403 Forbidden,未登录返回 401 Unauthorized
Before
之前的实现(基于 13.x 版本)详细可以参考:https://www.cnblogs.com/weihanli/p/custom-authentication-authorization-in-ocelot.html
大致代码如下:
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<UrlBasedAuthenticationMiddleware>()){_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<ApiPermission>("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)// 完全匹配不到,再根据正则匹配{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)) //默认需要登录才能访问{//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
来看一下在新版本(16.x/17.x)的 ocelot 中实现代码是怎样的
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<UrlBasedAuthenticationMiddleware>()){_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<ApiPermission>("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
主要的区别在于 ocelot 中间件的变化,在之前的版本,ocelot 是自己的中间件,签名是 Task Invoke(DownstreamContext context) 是 ocelot 自己的 DownstreamContext,在之后 ,Ocelot 为了和 asp.net core 中间件保持一样的签名,以更好的复用 asp.net core 中的中间件,更新了自己的中间件, ocelot 自己的 context 等信息现在放在了 HttpContext.Items 中,并通过一系列的扩展方法来获取和更新对应的信息
但是目前的实现并不能够完全等同于 asp.net core 中间件,因为如果你想要中断某一个中间件的话现在大概是有问题的,因为现在 ocelot 中间件里的
HttpContext并不是原始的HttpContextocelot 会在真正开始处理请求之前新建一个HttpContext把基本的请求信息复制过去,主要实现代码: https://github.com/ThreeMammals/Ocelot/blob/17.0.0/src/Ocelot/Multiplexer/MultiplexingMiddleware.cs如果想要在自定义中间件中实现中断,需要使用 ocelot 的中间件,通过 SetError 来去处理而不要直接使用
httpContext.Response去中断请求
API Diff
中间件
Invoke方法签名,从原来的Task Invoke(DownstreamContext context)更新成Task Invoke(HttpContext context)SetPipelineError不再是OcelotMiddleware中的一个方法,通过httpContext.Items.SetError方法来代替通过
httpContext.Items.DownstreamRoute()来获取当前请求的DownstreamRoute信息
More
除了中间件的变化,配置也发生了变化,原来的 ReRoute 也变成了 Route,升级的时候需要注意一下配置的变化,否则可能就会 404 了,在 17.0 之后,authorisation 更新成了 authorization, authorise 也更新成了 authorize
更多更新可以参考 ocelot 的 PR changes 和文档
文中提到的示例在 Github 上可以获取完整的代码 https://github.com/WeihanLi/AspNetCorePlayground/tree/master/OcelotDemo
Reference
https://github.com/ThreeMammals/Ocelot/compare/15.0.0...16.0.0
https://github.com/ThreeMammals/Ocelot/compare/16.0.0...17.0.0
https://github.com/WeihanLi/AspNetCorePlayground/tree/master/OcelotDemo
https://www.cnblogs.com/weihanli/p/custom-authentication-authorization-in-ocelot.html