关于ASP.NET MVC:ASP.NET MVC – 设置自定义IIdentity或IPrincipal

ASP.NET MVC - Set custom IIdentity or IPrincipal

我需要做一些相当简单的事情:在我的ASP.NET MVC应用程序中,我想设置一个自定义的iIdentity/iprincipal。更容易/更合适。我想扩展默认值,这样我可以调用类似于User.Identity.IdUser.Identity.Role的东西。没什么特别的,只是一些额外的财产。

我读过很多文章和问题,但我觉得我比实际情况更难理解。我觉得这很容易。如果用户登录,我想设置一个自定义的iIdentity。所以我想,我会在global.asax中实现Application_PostAuthenticateRequest。但是,它在每个请求上都被调用,我不想在每个请求上调用数据库,这些请求将从数据库请求所有数据并放入自定义的IPrincipal对象。这看起来也很不必要,很慢,而且在错误的地方(在那里进行数据库调用),但我可能是错的。或者这些数据来自哪里?

所以我想,每当用户登录时,我可以在会话中添加一些必要的变量,这些变量将添加到Application_PostAuthenticateRequest事件处理程序中的自定义IIdentity中。不过,我的Context.Sessionnull那里,所以这也不是去的路。

我已经为此工作了一天了,我觉得我错过了一些东西。这不应该太难,对吧?我也有点困惑的所有(半)相关的东西,这与此。MembershipProviderMembershipUserRoleProviderProfileProviderIPrincipalIIdentityFormsAuthentication…我是唯一一个发现这一切都很混乱的人吗?

如果有人能告诉我一个简单、优雅、高效的解决方案,在一个iIdentity上存储一些额外的数据,而不需要所有额外的模糊。那太好了!我知道有类似的问题,但如果我需要的答案在那里,我一定忽略了。


我就是这样做的。

我决定使用iprincipal而不是iIdentity,因为这意味着我不必同时实现iIdentity和iprincipal。

  • 创建接口

    1
    2
    3
    4
    5
    6
    interface ICustomPrincipal : IPrincipal
    {
        int Id { get; set; }
        string FirstName { get; set; }
        string LastName { get; set; }
    }
  • 客户主体

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class CustomPrincipal : ICustomPrincipal
    {
        public IIdentity Identity { get; private set; }
        public bool IsInRole(string role) { return false; }

        public CustomPrincipal(string email)
        {
            this.Identity = new GenericIdentity(email);
        }

        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }
  • CustomPrincipalSerializeModel-用于将自定义信息序列化到FormsAuthenticationTicket对象的UserData字段中。

    1
    2
    3
    4
    5
    6
    public class CustomPrincipalSerializeModel
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }
  • 登录方法-使用自定义信息设置cookie

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    if (Membership.ValidateUser(viewModel.Email, viewModel.Password))
    {
        var user = userRepository.Users.Where(u => u.Email == viewModel.Email).First();

        CustomPrincipalSerializeModel serializeModel = new CustomPrincipalSerializeModel();
        serializeModel.Id = user.Id;
        serializeModel.FirstName = user.FirstName;
        serializeModel.LastName = user.LastName;

        JavaScriptSerializer serializer = new JavaScriptSerializer();

        string userData = serializer.Serialize(serializeModel);

        FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket(
                 1,
                 viewModel.Email,
                 DateTime.Now,
                 DateTime.Now.AddMinutes(15),
                 false,
                 userData);

        string encTicket = FormsAuthentication.Encrypt(authTicket);
        HttpCookie faCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encTicket);
        Response.Cookies.Add(faCookie);

        return RedirectToAction("Index","Home");
    }
  • global.asax.cs-读取cookie并替换httpcontext.user对象,这是通过重写postAuthenticateRequest完成的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    protected void Application_PostAuthenticateRequest(Object sender, EventArgs e)
    {
        HttpCookie authCookie = Request.Cookies[FormsAuthentication.FormsCookieName];

        if (authCookie != null)
        {
            FormsAuthenticationTicket authTicket = FormsAuthentication.Decrypt(authCookie.Value);

            JavaScriptSerializer serializer = new JavaScriptSerializer();

            CustomPrincipalSerializeModel serializeModel = serializer.Deserialize<CustomPrincipalSerializeModel>(authTicket.UserData);

            CustomPrincipal newUser = new CustomPrincipal(authTicket.Name);
            newUser.Id = serializeModel.Id;
            newUser.FirstName = serializeModel.FirstName;
            newUser.LastName = serializeModel.LastName;

            HttpContext.Current.User = newUser;
        }
    }
  • 在Razor视图中访问

    1
    2
    3
    @((User as CustomPrincipal).Id)
    @((User as CustomPrincipal).FirstName)
    @((User as CustomPrincipal).LastName)
  • 在代码中:

    1
    2
    3
        (User as CustomPrincipal).Id
        (User as CustomPrincipal).FirstName
        (User as CustomPrincipal).LastName

    我认为代码是不言而喻的。如果不是,请告诉我。

    此外,为了使访问更加容易,您可以创建一个基本控制器并重写返回的用户对象(httpcontext.user):

    1
    2
    3
    4
    5
    6
    7
    public class BaseController : Controller
    {
        protected virtual new CustomPrincipal User
        {
            get { return HttpContext.User as CustomPrincipal; }
        }
    }

    然后,对于每个控制器:

    1
    2
    3
    4
    public class AccountController : BaseController
    {
        // ...
    }

    这将允许您访问以下代码中的自定义字段:

    1
    2
    3
    User.Id
    User.FirstName
    User.LastName

    但这在视图内部不起作用。为此,您需要创建自定义的WebViewPage实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public abstract class BaseViewPage : WebViewPage
    {
        public virtual new CustomPrincipal User
        {
            get { return base.User as CustomPrincipal; }
        }
    }

    public abstract class BaseViewPage<TModel> : WebViewPage<TModel>
    {
        public virtual new CustomPrincipal User
        {
            get { return base.User as CustomPrincipal; }
        }
    }

    使其成为视图/web.config中的默认页面类型:

    1
    2
    3
    4
    5
    6
    7
    8
    <pages pageBaseType="Your.Namespace.BaseViewPage">
      <namespaces>
       
       
       
       
      </namespaces>
    </pages>

    在视图中,您可以这样访问它:

    1
    2
    @User.FirstName
    @User.LastName


    我不能直接说ASP.NET MVC,但对于ASP.NET Web窗体,技巧是创建一个FormsAuthenticationTicket,并在用户通过身份验证后将其加密到cookie中。这样,您只需调用数据库一次(或AD或任何用于执行身份验证的对象),然后每个后续请求都将基于存储在cookie中的票证进行身份验证。

    关于这方面的一篇好文章:http://www.on dotnet.com/pub/a/dotnet/2004/02/02/effectiveformsauth.html(broken link)

    编辑:

    由于上面的链接被破坏,我建议Lukep在上面的答案中给出解决方案:https://stackoverflow.com/a/10524305-我还建议将接受的答案改为该答案。

    编辑2:断开链接的替代方法:https://web.archive.org/web/20120422011422/http://ondotnet.com/pub/a/dotnet/2004/02/02/effectiveformsauth.html


    下面是一个完成这项工作的例子。bool isvalid是通过查看一些数据存储来设置的(比如用户数据库)。用户ID只是我正在维护的一个ID。您可以将电子邮件地址等附加信息添加到用户数据中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    protected void btnLogin_Click(object sender, EventArgs e)
    {        
        //Hard Coded for the moment
        bool isValid=true;
        if (isValid)
        {
             string userData = String.Empty;
             userData = userData +"UserID=" + userID;
             FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(1, username, DateTime.Now, DateTime.Now.AddMinutes(30), true, userData);
             string encTicket = FormsAuthentication.Encrypt(ticket);
             HttpCookie faCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encTicket);
             Response.Cookies.Add(faCookie);
             //And send the user where they were heading
             string redirectUrl = FormsAuthentication.GetRedirectUrl(username, false);
             Response.Redirect(redirectUrl);
         }
    }

    在golbal asax中添加以下代码以检索您的信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    protected void Application_AuthenticateRequest(Object sender, EventArgs e)
    {
        HttpCookie authCookie = Request.Cookies[
                 FormsAuthentication.FormsCookieName];
        if(authCookie != null)
        {
            //Extract the forms authentication cookie
            FormsAuthenticationTicket authTicket =
                   FormsAuthentication.Decrypt(authCookie.Value);
            // Create an Identity object
            //CustomIdentity implements System.Web.Security.IIdentity
            CustomIdentity id = GetUserIdentity(authTicket.Name);
            //CustomPrincipal implements System.Web.Security.IPrincipal
            CustomPrincipal newUser = new CustomPrincipal();
            Context.User = newUser;
        }
    }

    当您稍后要使用这些信息时,可以按如下方式访问您的自定义主体。

    1
    2
    3
    (CustomPrincipal)this.User
    or
    (CustomPrincipal)this.Context.User

    这将允许您访问自定义用户信息。


    MVC为您提供了挂起在控制器类中的OnAuthorize方法。或者,可以使用自定义操作筛选器来执行授权。MVC使操作非常简单。我在这里发表了一篇关于这个的博客文章。http://www.bradygaster.com/post/custom-authentication-with-mvc-3.0


    如果需要将一些方法连接到@user以在视图中使用,这里有一个解决方案。对于任何正式的成员定制都没有解决方案,但是如果仅视图需要原始问题,那么这可能就足够了。下面用于检查从authorizefilter返回的变量,用于验证此处是否存在某些链接(不用于任何类型的授权逻辑或访问授权)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    using System;
        using System.Collections.Generic;
        using System.Linq;
        using System.Web;
        using System.Security.Principal;

        namespace SomeSite.Web.Helpers
        {
            public static class UserHelpers
            {
                public static bool IsEditor(this IPrincipal user)
                {
                    return null; //Do some stuff
                }
            }
        }

    然后只需在areas web.config中添加一个引用,并在视图中如下所示调用它。

    1
    @User.IsEditor()


    好吧,我是一个严肃的密码管理员,我把这个非常古老的问题拖了上来,但有一个更简单的方法,上面的@baserz提到了这个方法。这就是使用C扩展方法和缓存(不要使用会话)的组合。

    实际上,微软已经在Microsoft.AspNet.Identity.IdentityExtensions名称空间中提供了许多这样的扩展。例如,GetUserId()是返回用户ID的扩展方法,还有GetUserName()FindFirstValue()两种方法,它们根据iprincipal返回索赔。

    因此,您只需要包含名称空间,然后调用User.Identity.GetUserName()以获取由ASP.NET标识配置的用户名。

    我不确定这是否是缓存的,因为旧的ASP.NET标识不是开放源代码的,而且我没有费心对其进行反向工程。但是,如果不是这样,那么您可以编写自己的扩展方法,这将在特定的时间段内缓存此结果。


    根据Lukep的回答,增加了与Web.config合作的timeoutrequireSSL的设置方法。

    参考链接

    • msdn,解释:ASP.NET 2.0中的窗体身份验证
    • msdn,formsAuthentication类
    • 代码中的.NET访问表单身份验证"超时"值

    修改后的lukep代码

    1、根据Web.config设置timeout。FormsAuthentication.Timeout将获取在web.config中定义的超时值。我将以下内容包装为一个函数,它返回一个ticket

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    int version = 1;
    DateTime now = DateTime.Now;

    // respect to the `timeout` in Web.config.
    TimeSpan timeout = FormsAuthentication.Timeout;
    DateTime expire = now.Add(timeout);
    bool isPersist = false;

    FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
         version,          
         name,
         now,
         expire,
         isPersist,
         userData);

    2,根据requireSSL配置,将cookie配置为安全或不安全。

    1
    2
    3
    4
    HttpCookie faCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encTicket);
    // respect to `RequreSSL` in `Web.Config`
    bool bSSL = FormsAuthentication.RequireSSL;
    faCookie.Secure = bSSL;

    作为对Web表单用户(不是MVC)lukep代码的补充,如果您想简化页面后面代码的访问,只需将下面的代码添加到一个基页,并在所有页面中派生出基页:

    1
    2
    3
    4
    5
    Public Overridable Shadows ReadOnly Property User() As CustomPrincipal
        Get
            Return DirectCast(MyBase.User, CustomPrincipal)
        End Get
    End Property

    因此,在您的代码背后,您可以简单地访问:

    1
    User.FirstName or User.LastName

    在Web表单场景中,我缺少的是如何在未绑定到页面的代码中获得相同的行为,例如在httpmodules中,我应该始终在每个类中添加一个强制转换,还是有一种更聪明的方法来获得这种行为?

    感谢您的回答,感谢Lukep,因为我使用了您的示例作为我的自定义用户的基础(现在有User.RolesUser.TasksUser.HasPath(int)User.Settings.Timeout和许多其他好东西)。


    我尝试了lukep建议的解决方案,发现它不支持authorize属性。所以我修改了一下。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    public class UserExBusinessInfo
    {
        public int BusinessID { get; set; }
        public string Name { get; set; }
    }

    public class UserExInfo
    {
        public IEnumerable<UserExBusinessInfo> BusinessInfo { get; set; }
        public int? CurrentBusinessID { get; set; }
    }

    public class PrincipalEx : ClaimsPrincipal
    {
        private readonly UserExInfo userExInfo;
        public UserExInfo UserExInfo => userExInfo;

        public PrincipalEx(IPrincipal baseModel, UserExInfo userExInfo)
            : base(baseModel)
        {
            this.userExInfo = userExInfo;
        }
    }

    public class PrincipalExSerializeModel
    {
        public UserExInfo UserExInfo { get; set; }
    }

    public static class IPrincipalHelpers
    {
        public static UserExInfo ExInfo(this IPrincipal @this) => (@this as PrincipalEx)?.UserExInfo;
    }


        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public async Task<ActionResult> Login(LoginModel details, string returnUrl)
        {
            if (ModelState.IsValid)
            {
                AppUser user = await UserManager.FindAsync(details.Name, details.Password);

                if (user == null)
                {
                    ModelState.AddModelError("","Invalid name or password.");
                }
                else
                {
                    ClaimsIdentity ident = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
                    AuthManager.SignOut();
                    AuthManager.SignIn(new AuthenticationProperties { IsPersistent = false }, ident);

                    user.LastLoginDate = DateTime.UtcNow;
                    await UserManager.UpdateAsync(user);

                    PrincipalExSerializeModel serializeModel = new PrincipalExSerializeModel();
                    serializeModel.UserExInfo = new UserExInfo()
                    {
                        BusinessInfo = await
                            db.Businesses
                            .Where(b => user.Id.Equals(b.AspNetUserID))
                            .Select(b => new UserExBusinessInfo { BusinessID = b.BusinessID, Name = b.Name })
                            .ToListAsync()
                    };

                    JavaScriptSerializer serializer = new JavaScriptSerializer();

                    string userData = serializer.Serialize(serializeModel);

                    FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket(
                             1,
                             details.Name,
                             DateTime.Now,
                             DateTime.Now.AddMinutes(15),
                             false,
                             userData);

                    string encTicket = FormsAuthentication.Encrypt(authTicket);
                    HttpCookie faCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encTicket);
                    Response.Cookies.Add(faCookie);

                    return RedirectToLocal(returnUrl);
                }
            }
            return View(details);
        }

    最后在global.asax.cs

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
        protected void Application_PostAuthenticateRequest(Object sender, EventArgs e)
        {
            HttpCookie authCookie = Request.Cookies[FormsAuthentication.FormsCookieName];

            if (authCookie != null)
            {
                FormsAuthenticationTicket authTicket = FormsAuthentication.Decrypt(authCookie.Value);
                JavaScriptSerializer serializer = new JavaScriptSerializer();
                PrincipalExSerializeModel serializeModel = serializer.Deserialize<PrincipalExSerializeModel>(authTicket.UserData);
                PrincipalEx newUser = new PrincipalEx(HttpContext.Current.User, serializeModel.UserExInfo);
                HttpContext.Current.User = newUser;
            }
        }

    现在我只需调用

    1
    User.ExInfo()

    要注销,我只需打电话

    1
    AuthManager.SignOut();

    AuthManager在哪里

    1
    HttpContext.GetOwinContext().Authentication