- A+
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
并不是原始的HttpContext
ocelot 会在真正开始处理请求之前新建一个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 和文档