ASP.NET 每个页面上的 MVC 自定义用户字段

ASP.NET MVC Custom user fields on every page

背景: 我正在构建越来越多的 Web 应用程序,其中设计师/模板制作者决定添加 "profile picture" 和其他一些与用户相关的数据,当然只有当有人登录时。

作为大多数 ASP.NET MVC 开发人员,我使用视图模型为剃须刀布局提供我需要显示的信息,这些信息来自存储库等。

使用

很容易显示用户名
HttpContext.Current.User.Identity.Name

如果我想在这些页面上显示保存在我的后备数据存储中的信息怎么办? ApplicationUser class 中的自定义字段,例如业务单位名称或个人资料图片 CDN url。

(为简单起见,假设我将身份框架与包含我的 ApplicationUsers 的 Entity Framework(SQL 数据库)一起使用)

问题

你是如何解决这个问题的:

  1. 不污染 viewmodel/controller 树(例如构建 BaseViewModel 或 BaseController 填充/提供此信息?
  2. 无需为这些详细信息的每个页面请求往返数据库?
  3. 用户未登录不查询数据库?
  4. 当您无法使用 SESSION 数据时(因为我的应用程序通常在多个 Azure 实例上扩展 - read why this isn't possible here- 我对 SQL 缓存或 Redis 缓存不感兴趣。

我考虑过使用局部视图来新建它们自己的视图模型 - 但是这仍然会在每次页面加载时往返于 SQL 数据库。会话数据目前是安全的,但在 azure 中扩展时,这也不是一种方法。知道我最好的选择是什么吗?

TLDR;

如果用户已登录(匿名访问 = 允许),我想在应用程序的每个页面上显示用户配置文件信息 (ApplicationUser)。如何在每个页面请求不查询数据库的情况下显示此信息?如果没有 Session class,我该怎么做?我如何在不构建基础 classes 的情况下执行此操作?

我认为 "how to" 有点主观,因为可能有很多可能的方法来解决这个问题,但我通过使用与 HttpContext 相同的模式解决了这个确切的问题。我创建了一个 class 名为 ApplicationContext 的静态实例 属性,returns 是一个使用 DI 的实例。 (如果您出于某种原因没有使用 DI,也可以更改 属性 以生成单例本身。)

public interface IApplicationContext
{
    //Interface
    string GetUsername();
}

public class ApplicationContext : IApplicationContext
{
    public static IApplicationContext Current
    {
        get
        {
            return DependencyResolver.Current.GetService<IApplicationContext>();
        }
    }


    //appropriate functions to get required data
    public string GetUsername() {
        if (HttpContext.Current.User.Identity.IsAuthenticated)
        {
            return HttpContext.Current.User.Identity.Name;
        }
        return null;
    }
}

那么你直接在你的视图中引用 "Current" 属性。

@ApplicationContext.Current.GetUsername()

这将解决除#2 之外的所有需求。数据库调用可能不会增加足够大的开销以保证完全避免,但如果您需要它,那么您唯一的选择是在第一次查询用户数据后实施某种形式的缓存。

简单地实现带缓存的子操作并因登录用户而异

您可以将您的额外信息存储为声明。在您的登录方法中,将您的数据填写到生成的身份中。例如,如果您使用身份的默认配置,您可以在 ApplicationUser.GenerateUserIdentityAsync() 方法中添加声明:

public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser, string> manager)
{
    // Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
    var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);

   // Add custom user claims here
   userIdentity.AddClaims(new[]
   {
       new Claim("MyValueName1","value1"),
       new Claim("MyValueName2","value2"),
       new Claim("MyValueName2","value3"),
       // and so on
   });

   return userIdentity;
}

并且在您的整个应用程序中,您可以通过阅读当前用户声明来访问这些信息。实际上 HttpContext.Current.User.Identity.Name 使用相同的方法。

public ActionResult MyAction()
{
     // you have access the authenticated user's claims 
     // simply by casting User.Identity to ClaimsIdentity
     var claims = ((ClaimsIdentity)User.Identity).Claims;
     // or 
     var claims2 = ((ClaimsIdentity)HttpContext.Current.User.Identity).Claims;
} 

Identity 的最佳方式是使用声明来存储有关用户的自定义数据。山姆的回答与我在这里所说的非常接近。我会详细说明一下。

ApplicationUser class 上,您有 GenerateUserIdentityAsync 方法用于创建 ClaimsIdentity 用户:

public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser, string> manager)
{
    // Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
    var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);

   // Add custom user claims here
   userIdentity.AddClaims(new[]
   {
       new Claim("MyApp:FirstName",this.FirstName), //presuming FirstName is part of ApplicationUser class
       new Claim("MyApp:LastName",this.LastName),
   });

   return userIdentity;
}

这会在最终在身份验证 cookie 中序列化和加密的用户身份上添加键值对 - 记住这一点很重要。

用户登录后,您可以通过 HttpContext.Current.User.Identity 使用此身份 - 该对象实际上是 ClaimsIdentity,声明来自 cookie。因此,无论您对登录时间提出什么要求,您都可以使用,而无需深入了解您的数据库。

为了从声明中获取数据,我通常在 IPrincipal

上使用扩展方法
public static String GetFirstName(this IPrincipal principal)
{
    var claimsPrincipal = principal as ClaimsPrincipal;
    if (claimsPrincipal == null)
    {
        throw new DomainException("User is not authenticated");
    }

    var personNameClaim = claimsPrincipal.Claims.FirstOrDefault(c => c.Type == "MyApp:FirstName");
    if (personNameClaim != null)
    {
        return personNameClaim.Value;
    }

    return String.Empty;
}

通过这种方式,您可以从 Razor 视图访问您的索赔数据:User.GetFirstName()

而且此操作非常快,因为它不需要来自 DI 容器的任何对象解析,也不会查询数据库。

唯一的障碍是当存储中的值实际更新时,auth cookie 中的声明中的值在用户注销和登录之前不会刷新。但是您可以通过 IAuehtenticationManager.Signout() 强制执行此操作,并立即使用更新后的声明值重新登录。