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 和文檔
文中提到的示例在 Github 上可以獲取完整的代碼 https://github.com/WeihanLi/AspNetCorePlayground/tree/master/OcelotDemo