我如何为ASP.NET MVC 2创建自定义会员提供程序?

58

我如何基于ASP.NET成员资格提供程序创建ASP.NET MVC 2的自定义成员资格?

4个回答

117

我创建了一个新项目,其中包含自定义成员资格提供程序,并重写了MembershipProvider抽象类的ValidateUser方法:

public class MyMembershipProvider : MembershipProvider
{ 
    public override bool ValidateUser(string username, string password)
    {    
        // this is where you should validate your user credentials against your database.
        // I've made an extra class so i can send more parameters 
        // (in this case it's the CurrentTerritoryID parameter which I used as 
        // one of the MyMembershipProvider class properties). 

        var oUserProvider = new MyUserProvider();  
        return oUserProvider.ValidateUser(username,password,CurrentTerritoryID);
    }
}

然后,我通过添加引用并在web.config文件中指定该提供程序的位置,将其连接到我的ASP.NET MVC 2项目:

<membership defaultProvider="MyMembershipProvider">
    <providers>
        <clear />
        <add name="MyMembershipProvider"
            applicationName="MyApp"
            Description="My Membership Provider"
            passwordFormat="Clear"
            connectionStringName="MyMembershipConnection"
            type="MyApp.MyMembershipProvider" />
    </providers>
</membership>

我需要创建一个自定义类,继承RoleProvider抽象类并重写GetRolesForUser方法。ASP.NET MVC授权使用该方法查找分配给当前登录用户的角色,并确保用户有权访问控制器操作。

以下是我们需要执行的步骤:

1) 创建一个自定义类,继承RoleProvider抽象类并重写GetRolesForUser方法:

public override string[] GetRolesForUser(string username)
{
    SpHelper db = new SpHelper();
    DataTable roleNames = null;
    try
    {
        // get roles for this user from DB...

        roleNames = db.ExecuteDataset(ConnectionManager.ConStr,
                    "sp_GetUserRoles",
                    new MySqlParameter("_userName", username)).Tables[0];
    }
    catch (Exception ex)
    {
        throw ex;
    }
    string[] roles = new string[roleNames.Rows.Count];
    int counter = 0;
    foreach (DataRow row in roleNames.Rows)
    {
        roles[counter] = row["Role_Name"].ToString();
        counter++;
    }
    return roles;
}

2) 通过我们的web.config文件将角色提供程序与ASP.NET MVC 2应用程序连接:

<system.web>
...

<roleManager enabled="true" defaultProvider="MyRoleProvider">
    <providers>
        <clear />
        <add name="MyRoleProvider"
            applicationName="MyApp"
            type="MyApp.MyRoleProvider"
            connectionStringName="MyMembershipConnection" />
    </providers>
</roleManager>

...
</system.web>

3) 在所需的控制器/操作上方设置Authorize(Roles="xxx,yyy"):

[Authorization(Roles = "Customer Manager,Content Editor")]
public class MyController : Controller
{
    ...... 
}

搞定了!现在它可以工作了!

4) 可选:设置自定义的 Authorize 属性,这样我们就可以将不想要的角色重定向到一个“拒绝访问”页面:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public class MyAuthorizationAttribute : AuthorizeAttribute
{
    /// <summary>
    /// The name of the master page or view to use when rendering the view on authorization failure.  Default
    /// is null, indicating to use the master page of the specified view.
    /// </summary>
    public virtual string MasterName { get; set; }

    /// <summary>
    /// The name of the view to render on authorization failure.  Default is "Error".
    /// </summary>
    public virtual string ViewName { get; set; }

    public MyAuthorizationAttribute ()
        : base()
    {
        this.ViewName = "Error";
    }

    protected void CacheValidateHandler(HttpContext context, object data, ref HttpValidationStatus validationStatus)
    {
        validationStatus = OnCacheAuthorization(new HttpContextWrapper(context));
    }

    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        if (filterContext == null)
        {
            throw new ArgumentNullException("filterContext");
        }

        if (AuthorizeCore(filterContext.HttpContext))
        {
            SetCachePolicy(filterContext);
        }
        else if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
        {
            // auth failed, redirect to login page
            filterContext.Result = new HttpUnauthorizedResult();
        }
        else if (filterContext.HttpContext.User.IsInRole("SuperUser"))
        {
            // is authenticated and is in the SuperUser role
            SetCachePolicy(filterContext);
        }
        else
        {
            ViewDataDictionary viewData = new ViewDataDictionary();
            viewData.Add("Message", "You do not have sufficient privileges for this operation.");
            filterContext.Result = new ViewResult { MasterName = this.MasterName, ViewName = this.ViewName, ViewData = viewData };
        }
    }

    protected void SetCachePolicy(AuthorizationContext filterContext)
    {
        // ** IMPORTANT **
        // Since we're performing authorization at the action level, the authorization code runs
        // after the output caching module. In the worst case this could allow an authorized user
        // to cause the page to be cached, then an unauthorized user would later be served the
        // cached page. We work around this by telling proxies not to cache the sensitive page,
        // then we hook our custom authorization code into the caching mechanism so that we have
        // the final say on whether a page should be served from the cache.
        HttpCachePolicyBase cachePolicy = filterContext.HttpContext.Response.Cache;
        cachePolicy.SetProxyMaxAge(new TimeSpan(0));
        cachePolicy.AddValidationCallback(CacheValidateHandler, null /* data */);
    }
}

现在我们可以使用自己制作的属性将用户重定向到访问被拒绝的视图:

[MyAuthorization(Roles = "Portal Manager,Content Editor", ViewName = "AccessDenied")]
public class DropboxController : Controller
{ 
    .......
}

就是这样! 非常好!

以下是我用来获取所有这些信息的一些链接:

自定义角色提供程序: http://davidhayden.com/blog/dave/archive/2007/10/17/CreateCustomRoleProviderASPNETRolePermissionsSecurity.aspx

希望这些信息能够帮到你!


你解释的方式太棒了!我敢打赌你甚至没有尽全力...你应该考虑写博客文章 :)。 - Erx_VB.NExT.Coder
2
谢谢伙计,很高兴能帮上忙。我经常这样做,通过这样做我更好地理解了它 :-) - danfromisrael
1
第一个代码片段中的"new MyUserProvider();"和"CurrentTerritoryID"是什么意思?这看起来相当简单,所以我希望这能完成工作:)谢谢! - ilija veselica
1
嗨ile, 基本上,ValidateUser方法是您应该针对数据库验证用户凭据的地方。我创建了一个额外的类,以便可以发送更多参数(在这种情况下,它是CurrentTerritoryID参数,我将其用作MyMembershipProvider类之一)。 尽管如此,您可以选择仅在那里验证它或通过另一个层/类/方法进行验证。 - danfromisrael
1
这是因为MembershipProvider是一个抽象类,其中包含必须在您的类中实现的抽象方法(如果您愿意,这些方法可以为空且不执行任何操作)。关于ASPNETDB文件,它是Microsoft成员资格架构的默认文件。您可以要求成员资格提供程序在您的DB服务器上使用相同的架构而不是文件(使用连接字符串),或者使用自定义成员资格提供程序使用自己的表,就像我们在这里所做的那样... - danfromisrael
显示剩余10条评论

10

你能够总结一下回答用户问题的链接的主要观点并提供链接吗? - Ryan Gates
同样的网络存档链接 - 这里 - Nuhman

8

您也可以使用更少的代码来实现这一点,但我不确定这种方法是否同样安全,但它可以很好地与您使用的任何数据库配合使用。

在global.asax文件中:

protected void Application_AuthenticateRequest(object sender, EventArgs e)
    {
        if (HttpContext.Current.User != null)
        {
            if (HttpContext.Current.User.Identity.IsAuthenticated)
            {
                if (HttpContext.Current.User.Identity is FormsIdentity)
                {
                    FormsIdentity id =
                        (FormsIdentity)HttpContext.Current.User.Identity;
                    FormsAuthenticationTicket ticket = id.Ticket;

                    // Get the stored user-data, in this case, our roles
                    string userData = ticket.UserData;
                    string[] roles = userData.Split(',');
                    HttpContext.Current.User = new GenericPrincipal(id, roles);
                }
            }
        }
    }

这段代码的作用是从由FormsAuthenticationTicket生成的authCookie中读取角色信息。登录逻辑如下:
public class dbService
{
    private databaseDataContext db = new databaseDataContext();

    public IQueryable<vwPostsInfo> AllPostsAndDetails()
    {
        return db.vwPostsInfos;
    }

    public IQueryable<role> GetUserRoles(int userID)
    {
        return (from r in db.roles
                    join ur in db.UsersRoles on r.rolesID equals ur.rolesID
                    where ur.userID == userID
                    select r);
    }

    public IEnumerable<user> GetUserId(string userName)
    {
        return db.users.Where(u => u.username.ToLower() == userName.ToLower());
    }

    public bool logOn(string username, string password)
    {
        try
        {
            var userID = GetUserId(username);
            var rolesIQueryable = GetUserRoles(Convert.ToInt32(userID.Select(x => x.userID).Single()));
            string roles = "";
            foreach (var role in rolesIQueryable)
            {
                roles += role.rolesName + ",";
            }

            roles.Substring(0, roles.Length - 2);
            FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
                       1, // Ticket version
                       username, // Username associated with ticket
                       DateTime.Now, // Date/time issued
                       DateTime.Now.AddMinutes(30), // Date/time to expire
                       true, // "true" for a persistent user cookie
                       roles, // User-data, in this case the roles
                       FormsAuthentication.FormsCookiePath);// Path cookie valid for

            // Encrypt the cookie using the machine key for secure transport
            string hash = FormsAuthentication.Encrypt(ticket);
            HttpCookie cookie = new HttpCookie(
               FormsAuthentication.FormsCookieName, // Name of auth cookie
               hash); // Hashed ticket

            // Set the cookie's expiration time to the tickets expiration time
            if (ticket.IsPersistent) cookie.Expires = ticket.Expiration;

            // Add the cookie to the list for outgoing response
            HttpContext.Current.Response.Cookies.Add(cookie);

            return true;
        }
        catch
        {
            return (false);
        }
    }
}

我在数据库中使用了两个表来存储角色信息:Role表包括roleID和roleName两列,UsersRoles表包括userID和roleID两列。这样可以为多个用户分配多个角色,并且可以轻松添加/删除用户的角色等逻辑。例如,您可以使用[Authorize(Roles="Super Admin")]。希望这能帮到您。
编辑:我忘记添加密码检查了,但您只需在logOn方法中添加一个if语句,检查提供的用户名和密码是否正确,如果不正确则返回false。

等等,所以你把角色名称存储在认证 cookie 中?这不意味着用户可以在他们的 auth cookie 中放置任何想要的角色吗?我猜这没关系,因为他们必须解密 cookie 吗? - Pandincus
@Pandincus:是的,如果用户成功解密了cookie,那就是使用这种方法的缺点之一。我们可以进一步加密角色,并在global.asax中提供公钥以供后续解密。虽然不完美,但它能够完成工作并且并不复杂。 - Joakim

1
我使用了NauckIt.PostgreSQL提供程序的源代码作为基础,并对其进行修改以适应我的需求。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接