ASP.NET Core MVC 基于请求标识的不同路由(单租户+同站多租户)

时间:2021-01-06 20:12:32

标签: c# asp.net-mvc asp.net-core routes asp.net-mvc-routing

在任何人说任何话之前,我知道这是一个相当荒谬的要求,但这是客户想要的。

背景

我们有一个用于数据管理的棕地系统,允许两种不同类型的用户进行访问,我们将其称为 TenantAdmin

  • Tenant 用户只能访问自己的数据,并从其身份中的 TenantContext 声明中获取 TenantId
  • Admin 用户可以访问所有 Tenants 的所有数据,以及 少数管理员只查看,并通过会话状态驱动的机制获取特定 TenantContextTenant
  • 还有一些不需要 TenantContext(通常是静态信息等)的视图可以被两种类型的用户访问,有些也可以被匿名用户访问。

我们希望摆脱这种有状态实现,转而采用完全无状态系统。

问题

我们面临的问题是如何识别 TenantContext 用户在哪个 Admin 中工作,而无需将其存储在会话状态中。显而易见的解决方案是在所有需要 TenantContext 的网址前加上 TenantId,例如 https://example.com/{tenantId}/somecontroller/someaction,但是这样做当然也会影响 Tenant 用户的网址,他们目前不知道或需要知道他们的 TenantId,因为 Tenant 用户只能分配给一个 TenantId。不幸的是,客户已经明确表示这是不可接受的。

所以需要回答的问题是,“如何根据当前提出请求的用户构建一个既是单租户又是多租户的网站?” >

我的第一个想法转向了一些 url 重写中间件,我已经实现了半成功:

public enum UserRole
{
    Tenant,
    Admin
}

public class IdentityBasedMultiTenantMiddleware
{
    private readonly RequestDelegate _next;

    public IdentityBasedMultiTenantMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext httpContext)
    {
        if (!(httpContext.User?.Identity?.IsAuthenticated ?? false))
        {
            await _next(httpContext);
            return;
        }

        if (!Enum.TryParse<UserRole>(httpContext.User.FindFirst("Role")?.Value, out var role))
        {
            await _next(httpContext);
            return;
        }

        if (role != UserRole.Admin)
        {
            await _next(httpContext);
            return;
        }

        if (httpContext.Request.Path.Value == "/"
            || httpContext.Request.Path.Value.StartsWith("/admin"))
        {
            await _next(httpContext);
            return;
        }

        var pathSegments = httpContext.Request.Path.Value.Split("/", StringSplitOptions.RemoveEmptyEntries);
            
        if (!Guid.TryParse(pathSegments.FirstOrDefault(), out var tenantId))
        {
            httpContext.Response.Redirect("/admin/tenantsearch");
            return;
        }

        httpContext.Items.Add("TenantId", tenantId);
        httpContext.Request.Path = httpContext.Request.Path.Value.Remove(0, tenantId.ToString().Length + 1);
        httpContext.Request.PathBase = $"/{tenantId}";

        await _next(httpContext);
    }
}

注意。我将 tenantId 添加到 httpContext.Items 集合中是为了演示这个概念。

但是,这种方法存在问题:

  • 如您所见,我不得不为所有管理路由添加 /admin 前缀,并硬编码一个条件来处理此问题。所有不需要 TenantContext 的视图也需要它,从而为管理不需要 TenantContext 的任何路由留下相当讨厌的维护开销。
  • 当以 Admin 用户身份查看任何页面时,一旦通过将 TenantContext 添加到 url(例如 https://example.com/{tenantId}/somecontroller/someaction)来建立 TenantContext,因为 url 重写的工作方式,整个网站的所有链接(不仅仅是需要 {tenantId} 的链接)都以 IUrlHelperFactory 为前缀 - 虽然我怀疑这个特定问题可以通过 TenantContext 的自定义实现来解决,但是当然,还需要镜像相同的条件,并为管理页面等带来相同的维护开销。

然后我继续解决这个问题,同时仍然使用 url 重写。如果需要 TenantId 的路由总是包含 Tenant,而我们只为 TenantContext 用户重写这些路由怎么办?不幸的是,这与第一个 url 重写解决方案产生了相同的问题,因为需要在中间件中识别需要 HttpContext 的路由和条件来处理它们。

最终我认为这是 url 重写方法的一个限制,因为我们在 Routing 级别而不是 [Route("someroute")] 级别工作,在那里我们可能有更多关于做出决定的映射控制器/动作。

我继续研究如何使用 MVC 路由引擎实现这一点,但我看不到前进的方向 - 这里有什么我遗漏的吗?也许动态控制器路由?所有的想法将不胜感激。

其他几个问题:

  • 我们对现有网站混合使用属性路由 ([HttpGet("someroute")]/TenantId) 和基于约定的路由。我们正在转向纯粹基于属性的路由。
  • 我们还研究了构建一个标签助手,将包含 Admin 的查询字符串附加到 const Tododata = [ { id: 3, ext: 'to take a shower', completed: true }, { id: 5, ext: 'to brush teeth', completed: true }, ... ]; 用户的链接 - 这种方法有效,但我认为它很脏,难以维护(容易破坏) 并最终以错误的方式实现这一目标。

0 个答案:

没有答案