29

我正在开发一个 ASP.Net vNext / MVC6 项目。我开始掌握 ASP.Net 身份。

该类ApplicationUser显然是我应该添加任何其他用户属性的地方,这适用于 Entity Framework,并且我的其他属性按预期存储在数据库中。

但是,当我想从我的视图中访问当前登录用户的详细信息时,问题就出现了。具体来说,我有一个_loginPartial.cshtml我想在其中检索和显示用户的 Gravatar 图标,为此我需要电子邮件地址。

RazorView基类有一个 User 属性,它是一个ClaimsPrincipal. 如何从该User属性返回到我的ApplicationUser, 以检索我的自定义属性?

请注意,我不是在问如何找到信息;我知道如何ApplicationUserUser.GetUserId()值中查找。这更多是关于如何明智地处理这个问题的问题。具体来说,我不想:

  • 从我的视图中执行任何类型的数据库查找(关注点分离)
  • 必须为每个控制器添加逻辑以检索当前用户的详细信息(DRY 原则)
  • 必须为每个 ViewModel 添加一个用户属性。

这似乎是一个“横切关注点”,应该有一个集中的标准解决方案,但我觉得我错过了一块拼图。从视图中获取这些自定义用户属性的最佳方法是什么?

注意:似乎 MVC 团队在项目模板中绕过了这个问题,确保 UserName 属性始终设置为用户的电子邮件地址,巧妙地避免了他们执行此查找以获取用户的电子邮件地址的需要!这对我来说似乎有点欺骗,在我的解决方案中,用户的登录名可能是也可能不是他们的电子邮件地址,所以我不能依赖这个技巧(我怀疑我以后需要访问其他属性)。

4

6 回答 6

23

更新到原始答案:(这违反了操作的第一个要求,如果您有相同的要求,请参阅我的原始答案)您可以通过在 Razor 视图中引用 FullName 来修改声明并添加扩展文件(在我的原始解决方案中)作为:

@UserManager.GetUserAsync(User).Result.FullName

原答案:

这几乎只是这个 stackoverflow 问题和遵循本教程的较短示例。

假设您已经在“ApplicationUser.cs”中设置了属性以及适用的 ViewModel 和 View 进行注册。

使用“FullName”作为额外属性的示例:

修改“AccountController.cs”注册方法为:

    public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null)
        {
            ViewData["ReturnUrl"] = returnUrl;
            if (ModelState.IsValid)
            {
                var user = new ApplicationUser {
                    UserName = model.Email,
                    Email = model.Email,
                    FullName = model.FullName //<-ADDED PROPERTY HERE!!!
                };
                var result = await _userManager.CreateAsync(user, model.Password);
                if (result.Succeeded)
                {
                    //ADD CLAIM HERE!!!!
                    await _userManager.AddClaimAsync(user, new Claim("FullName", user.FullName)); 

                    await _signInManager.SignInAsync(user, isPersistent: false);
                    _logger.LogInformation(3, "User created a new account with password.");
                    return RedirectToLocal(returnUrl);
                }
                AddErrors(result);
            }

            return View(model);
        }

然后我添加了一个新文件“Extensions/ClaimsPrincipalExtension.cs”

using System.Linq;
using System.Security.Claims;
namespace MyProject.Extensions
    {
        public static class ClaimsPrincipalExtension
        {
            public static string GetFullName(this ClaimsPrincipal principal)
            {
                var fullName = principal.Claims.FirstOrDefault(c => c.Type == "FullName");
                return fullName?.Value;
            }   
        }
    }

然后在您需要访问该属性的视图中添加:

@using MyProject.Extensions

并在需要时通过以下方式调用它:

@User.GetFullName()

这样做的一个问题是我必须删除我当前的测试用户,然后重新注册才能看到“FullName”,即使数据库中有 FullName 属性。

于 2016-07-09T18:27:55.770 回答
18

我认为您应该为此目的使用用户的 Claims 属性。我找到了关于:http ://benfoster.io/blog/customising-claims-transformation-in-aspnet-core-identity 的好帖子

用户类

public class ApplicationUser : IdentityUser
{
    public string MyProperty { get; set; }
}

让我们将 MyProperty 放入已验证用户的声明中。为此,我们将覆盖 UserClaimsPrincipalFactory

public class MyUserClaimsPrincipalFactory : UserClaimsPrincipalFactory<ApplicationUser, IdentityRole>
{
    public MyUserClaimsPrincipalFactory (
        UserManager<ApplicationUser> userManager,
        RoleManager<IdentityRole> roleManager,
        IOptions<IdentityOptions> optionsAccessor) : base(userManager, roleManager, optionsAccessor)
    {
    }

    public async override Task<ClaimsPrincipal> CreateAsync(ApplicationUser user)
    {
        var principal = await base.CreateAsync(user);

        //Putting our Property to Claims
        //I'm using ClaimType.Email, but you may use any other or your own
        ((ClaimsIdentity)principal.Identity).AddClaims(new[] {
        new Claim(ClaimTypes.Email, user.MyProperty)
    });

        return principal;
    }
}

在 Startup.cs 中注册我们的 UserClaimsPrincipalFactory

public void ConfigureServices(IServiceCollection services)
{
    //...
    services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>, MyUserClaimsPrincipalFactory>();
    //...
}

现在我们可以像这样访问我们的财产

User.Claims.FirstOrDefault(v => v.Type == ClaimTypes.Email).Value;

我们可以创建一个扩展

namespace MyProject.MyExtensions
{
    public static class MyUserPrincipalExtension
    {
        public static string MyProperty(this ClaimsPrincipal user)
        {
            if (user.Identity.IsAuthenticated)
            {
                return user.Claims.FirstOrDefault(v => v.Type == ClaimTypes.Email).Value;
            }

            return "";
        }
    }
}

我们应该将@Using 添加到 View(我将它添加到全局 _ViewImport.cshtml)

@using MyProject.MyExtensions

最后我们可以在任何视图中使用这个属性作为方法调用

@User.MyProperty()

在这种情况下,您无需对数据库进行额外查询以获取用户信息。

于 2016-04-04T04:44:29.037 回答
2

我有同样的问题和同样的担忧,但是我选择了一个不同的解决方案,而不是为 ClaimsPrincipal 创建一个扩展方法,并让扩展方法检索自定义用户属性。

这是我的扩展方法:

public static class PrincipalExtensions
{
    public static string ProfilePictureUrl(this ClaimsPrincipal user, UserManager<ApplicationUser> userManager)
    {
        if (user.Identity.IsAuthenticated)
        {
            var appUser = userManager.FindByIdAsync(user.GetUserId()).Result;

            return appUser.ProfilePictureUrl;
        }

        return "";
    }
}

接下来在我的视图(也是 LoginPartial 视图)中,我注入 UserManager,然后将该 UserManager 传输到扩展方法:

@inject Microsoft.AspNet.Identity.UserManager<ApplicationUser> userManager;
<img src="@User.ProfilePictureUrl(userManager)">

我相信这个解决方案也满足您的 3 个关注点分离、DRY 和不更改任何 ViewModel 的要求。然而,虽然这个解决方案很简单并且不仅可以在标准视图中使用 ViewComponents,但我仍然不高兴。现在在我看来,我可以写: @User.ProfilePictureUrl(userManager),但我认为要求我只能写:@User.ProfilePictureUrl() 并不 过分

如果我可以在没有函数注入的情况下使 UserManager(或 IServiceProvider)在我的扩展方法中可用,它会解决问题,但我知道没有办法做到这一点。

于 2016-03-31T09:53:19.843 回答
2

好的,这就是我最终做到的方式。我在 MVC6 中使用了一个名为View Components的新功能。这些工作有点像局部视图,但它们有一个与之关联的“迷你控制器”。View Component 是一个不参与模型绑定的轻量级控制器,但它可以在构造函数参数中传递一些东西,可能使用依赖注入,然后它可以构造一个 View Model 并将其传递给局部视图。因此,例如,您可以将一个UserManager实例注入到视图组件中,使用它来检索ApplicationUser当前用户的对象并将其传递给部分视图。

这是它在代码中的样子。首先是视图组件,它位于/ViewComponents目录中:

public class UserProfileViewComponent : ViewComponent
    {
    readonly UserManager<ApplicationUser> userManager;

    public UserProfileViewComponent(UserManager<ApplicationUser> userManager)
        {
        Contract.Requires(userManager != null);
        this.userManager = userManager;
        }

    public IViewComponentResult Invoke([CanBeNull] ClaimsPrincipal user)
        {
        return InvokeAsync(user).WaitForResult();
        }

    public async Task<IViewComponentResult> InvokeAsync([CanBeNull] ClaimsPrincipal user)
        {
        if (user == null || !user.IsSignedIn())
            return View(anonymousUser);
        var userId = user.GetUserId();
        if (string.IsNullOrWhiteSpace(userId))
            return View(anonymousUser);
        try
            {
            var appUser = await userManager.FindByIdAsync(userId);
            return View(appUser ?? anonymousUser);
            }
        catch (Exception) {
        return View(anonymousUser);
        }
        }

    static readonly ApplicationUser anonymousUser = new ApplicationUser
        {
        Email = string.Empty,
        Id = "anonymous",
        PhoneNumber = "n/a"
        };
    }

注意userManager构造函数参数是由MVC框架注入的;这是Startup.cs在新项目中默认配置的,因此无需进行任何配置。

毫不奇怪,通过调用Invoke方法或它的异步版本来调用视图组件。如果可能,该方法检索一个ApplicationUser,否则它使用具有一些安全默认预配置的匿名用户。它使用该用户对其部分视图的视图模型。视图存在/Views/Shared/Components/UserProfile/Default.cshtml并开始如下:

@model ApplicationUser

<div class="dropdown profile-element">
    <span>
        @Html.GravatarImage(Model.Email, size:80)
    </span>
    <a data-toggle="dropdown" class="dropdown-toggle" href="#">
        <span class="clear">
            <span class="block m-t-xs">
                <strong class="font-bold">@Model.UserName</strong>
            </span> <span class="text-muted text-xs block">@Model.PhoneNumber <b class="caret"></b></span>
        </span>
    </a>

</div>

最后,我从我的_Navigation.cshtml局部视图中调用它,如下所示:

@await Component.InvokeAsync("UserProfile", User)

这符合我最初的所有要求,因为:

  1. 我在控制器(视图组件是一种控制器)中而不是在视图中执行数据库查找。此外,数据很可能已经在内存中,因为框架已经验证了请求。我还没有调查是否真的发生了另一个数据库往返,我可能不会打扰,但如果有人知道,请插话!
  2. 逻辑在一个明确定义的地方;尊重 DRY 原则。
  3. 我不必修改任何其他视图模型。

结果!我希望有人会发现这很有用...

于 2016-03-05T13:38:44.400 回答
1

正如我被问到的那样,我发布了我的最终解决方案,尽管是在不同的 (MVC5/EF6) 项目中。

首先,我定义了一个接口:

public interface ICurrentUser
    {
    /// <summary>
    ///     Gets the display name of the user.
    /// </summary>
    /// <value>The display name.</value>
    string DisplayName { get; }

    /// <summary>
    ///     Gets the login name of the user. This is typically what the user would enter in the login screen, but may be
    ///     something different.
    /// </summary>
    /// <value>The name of the login.</value>
    string LoginName { get; }

    /// <summary>
    ///     Gets the unique identifier of the user. Typically this is used as the Row ID in whatever store is used to persist
    ///     the user's details.
    /// </summary>
    /// <value>The unique identifier.</value>
    string UniqueId { get; }

    /// <summary>
    ///     Gets a value indicating whether the user has been authenticated.
    /// </summary>
    /// <value><c>true</c> if this instance is authenticated; otherwise, <c>false</c>.</value>
    bool IsAuthenticated { get; }

然后,我在一个具体的类中实现它:

/// <summary>
///     Encapsulates the concept of a 'current user' based on ASP.Net Identity.
/// </summary>
/// <seealso cref="MS.Gamification.DataAccess.ICurrentUser" />
public class AspNetIdentityCurrentUser : ICurrentUser
    {
    private readonly IIdentity identity;
    private readonly UserManager<ApplicationUser, string> manager;
    private ApplicationUser user;

    /// <summary>
    ///     Initializes a new instance of the <see cref="AspNetIdentityCurrentUser" /> class.
    /// </summary>
    /// <param name="manager">The ASP.Net Identity User Manager.</param>
    /// <param name="identity">The identity as reported by the HTTP Context.</param>
    public AspNetIdentityCurrentUser(ApplicationUserManager manager, IIdentity identity)
        {
        this.manager = manager;
        this.identity = identity;
        }

    /// <summary>
    ///     Gets the display name of the user. This implementation returns the login name.
    /// </summary>
    /// <value>The display name.</value>
    public string DisplayName => identity.Name;

    /// <summary>
    ///     Gets the login name of the user.
    ///     something different.
    /// </summary>
    /// <value>The name of the login.</value>
    public string LoginName => identity.Name;

    /// <summary>
    ///     Gets the unique identifier of the user, which can be used to look the user up in a database.
    ///     the user's details.
    /// </summary>
    /// <value>The unique identifier.</value>
    public string UniqueId
        {
        get
            {
            if (user == null)
                user = GetApplicationUser();
            return user.Id;
            }
        }

    /// <summary>
    ///     Gets a value indicating whether the user has been authenticated.
    /// </summary>
    /// <value><c>true</c> if the user is authenticated; otherwise, <c>false</c>.</value>
    public bool IsAuthenticated => identity.IsAuthenticated;

    private ApplicationUser GetApplicationUser()
        {
        return manager.FindByName(LoginName);
        }
    }

最后,我在我的 DI 内核中进行了以下配置(我正在使用 Ninject):

        kernel.Bind<ApplicationUserManager>().ToSelf()
            .InRequestScope();
        kernel.Bind<ApplicationSignInManager>().ToSelf().InRequestScope();
        kernel.Bind<IAuthenticationManager>()
            .ToMethod(m => HttpContext.Current.GetOwinContext().Authentication)
            .InRequestScope();
        kernel.Bind<IIdentity>().ToMethod(p => HttpContext.Current.User.Identity).InRequestScope();
        kernel.Bind<ICurrentUser>().To<AspNetIdentityCurrentUser>();

然后,每当我想访问当前用户时,我只需通过添加 type 的构造函数参数将其注入到我的控制器中ICurrentUser

我喜欢这个解决方案,因为它很好地封装了问题并避免我的控制器直接依赖于 EF。

于 2016-08-30T17:39:05.380 回答
0

您需要使用当前用户的名称进行搜索(例如使用实体框架):

HttpContext.Current.User.Identity.Name
于 2016-03-03T20:04:37.150 回答