首先说明,本文用的是非常屌丝的办法,针对SQL Server Membership的实现的一个很有局限性的权限模块,而不是真正对ASP.NET Membership本身的拓展。不喜慎入。

一、为什么要给Memebership增加权限系统


我们知道,ASP.NET Membership是基于Role的,其实是个RBAC,没有权限(Rights)功能。也就是说,我们只能够指定某个Role可以做什么事,丧失了一定的灵活性。比如,网站后台有个编辑文章的功能。如果基于Role判断,我只能认为:所有Admin、Editor、Teacher可以编辑文章。在MVC3里,可以用Authorize(Role=”Admin, Editor, Teacher”)的属性做到这一点。然而如果想要动态可配,则比较难了。然而,如果是基于Rights的验证,那我就可以认为:所有具有Edit权限的Role可以编辑文章。这样的话,如果那天我们不希望Teacher编辑文章,就可以把Teacher这个Role的Edit权限去掉,而不用改程序。下面我们看看如何在SQL Server上增加一个Rights功能。

二、数据库更改


假设你已经在SQL Server上配好了ASP.NET Memebership服务(可以使用aspnet_regsql工具),你就会看到一些列的表,其中有个叫“Roles”(也可能叫aspnet_Roles),我们需要做的是建另外两张表。

“Right”表包括了你应用程序中所有的权限枚举,这里我仅仅需要“RightName”字段来表示,你可以可以按需要增加其他字段。“RoleRight”是一张多对多的表,描述了“Roles have rights”的关系。

当然,你也可能需要在这几张表之间增加关联,并且把DELETE RULE设为CASCADE。不过,如果你实在不喜欢做外键关联也没关系,但最终码程序的时候你需要一个更复杂的查询来完成,文章后面会一起介绍。

创建表的SQL脚本如下:

CREATE TABLE [dbo].[Rights](
	[Id] [uniqueidentifier] NOT NULL,
	[RightName] [nvarchar](150) NOT NULL,
 CONSTRAINT [PK_Rights] PRIMARY KEY CLUSTERED 
(
	[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

GO
CREATE TABLE [dbo].[RoleRight](
	[RoleId] [uniqueidentifier] NOT NULL,
	[RightId] [uniqueidentifier] NOT NULL,
 CONSTRAINT [PK_RoleRight] PRIMARY KEY CLUSTERED 
(
	[RoleId] ASC,
	[RightId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

GO

ALTER TABLE [dbo].[RoleRight]  WITH CHECK ADD  CONSTRAINT [FK_RoleRight_Rights] FOREIGN KEY([RightId])
REFERENCES [dbo].[Rights] ([Id])
ON UPDATE CASCADE
ON DELETE CASCADE
GO

ALTER TABLE [dbo].[RoleRight] CHECK CONSTRAINT [FK_RoleRight_Rights]
GO

ALTER TABLE [dbo].[RoleRight]  WITH CHECK ADD  CONSTRAINT [FK_RoleRight_Roles] FOREIGN KEY([RoleId])
REFERENCES [dbo].[Roles] ([RoleId])
ON UPDATE CASCADE
ON DELETE CASCADE
GO

ALTER TABLE [dbo].[RoleRight] CHECK CONSTRAINT [FK_RoleRight_Roles]
GO

接下来,我们插一些测试数据:

如图,这些记录的意思是所有在Administrators这个Role下的User都具有Edit和Create权限。

三、示例代码:ASP.NET MVC3应用程序的代码更改


首先,我们要对MembershipUser类进行拓展,让它支持我们的权限表。假设你用的是Entity Framework,你可以像这样进行拓展:

public static class MembershipExtensions
{
    public static bool HasRight(this MembershipUser user, string[] rights)
    {
        var userRole = user.GetRoles();

        using (var db = new HammerEntities())
        {
            var query = from role in db.Roles
                      where userRole.Contains(role.RoleName)
                      select role.Rights into temp
                      from right in temp
                      where rights.Contains(right.RightName)
                      select right;

            return query.Count() == rights.Length;
        }
    }

    public static string[] GetRoles(this MembershipUser user)
    {
        return System.Web.Security.Roles.GetRolesForUser(user.UserName);
    }
}

测试一下这个方法,EF会在运行时产生以下SQL:

SELECT [Join1].[Id]         AS [Id],
       [Join1].[RightName]  AS [RightName]
FROM   [dbo].[Roles]        AS [Extent1]
       INNER JOIN (
                SELECT [Extent2].[RoleId] AS [RoleId],
                       [Extent3].[Id]     AS [Id],
                       [Extent3].[RightName] AS [RightName]
                FROM   [dbo].[RoleRight]  AS [Extent2]
                       INNER JOIN [dbo].[Rights] AS [Extent3]
                            ON  [Extent3].[Id] = [Extent2].[RightId]
            )               AS [Join1]
            ON  [Extent1].[RoleId] = [Join1].[RoleId]
WHERE  ([Join1].[RightName] IN (N'Edit', N'Create'))
       AND (N'Administrators' = [Extent1].[RoleName])

结果正如我们希望的一样,Administrators的所有权限被列出来了:

刚才在第二节最后提到,如果你不希望给表之间建立关联,必须使用一个复杂查询来完成同样的操作。这个查询如下,运行结果是一样的:

var query = from role in db.Roles
          join roleRight in db.RoleRight on role.RoleId equals roleRight.RoleId
          join right in db.Rights on roleRight.RightId equals right.Id
          where userRole.Contains(role.RoleName) && rights.Contains(right.RightName)
          select right;

现在,你的ASP.NET网站已经具有检查权限的功能了:

[Authorize]
public ActionResult About()
{
    var rightsToCheck = new string[] { "Edit", "Create" };
    if (CheckRight(rightsToCheck))
    {
        return View();
    }
    return new HttpUnauthorizedResult();
}

private bool CheckRight(string[] rights)
{
    var user = Membership.GetUser(User.Identity.Name);
    if (null != user)
    {
        return user.HasRight(rights);
    }
    return false;
}

并且,在ASP.NET MVC3的应用程序中,我们可以用一种更专业的手段去完成验证——作为Attribute。

只要重写AuthorizeAttribute类中的AuthorizeCore()方法,你就可以在MVC3中仅仅用一行代码完成权限验证。RightAuthorizeAttribute代码如下:

public class RightAuthorizeAttribute : AuthorizeAttribute
{
    public string Right { get; set; }

    protected override bool AuthorizeCore(HttpContextBase httpContext)
    {
        return CheckRight(Right.Split(','), httpContext);
    }

    private bool CheckRight(string[] rights, HttpContextBase httpContext)
    {
        var user = Membership.GetUser(httpContext.User.Identity.Name);
        if (null != user)
        {
            return user.HasRight(rights);
        }
        return false;
    }
}

现在,你可以用非常干净的代码来验证权限了:

[Authorize]
[RightAuthorize(Right = "Edit,Create")]
public ActionResult About()
{
    return View();
}

 试试访问“About”Action,因为要求登录,所以系统会弹回一个登录页面:

我是以管理员组的用户登录的,所以可以成功看到About页面的内容:

如果我把权限特征改成[RightAuthorize(Right = "Edit,View")],其中View并不是授权给Administrators的,那么这个Action会再次将我弹回登录页面,因为你没有View的权限了。可以从Debug中看到这一切:

OK,至此我们已经码完了最基本的ASP.NET Memebership权限验证模块~