ASP.NET MVC - Role Provider的替代方案是什么?

47

我试图避免使用Role Provider和Membership Provider,因为在我看来它们太过笨拙,因此我正在尝试制作自己的“版本”,这个版本更加简洁易管理/灵活。现在我的问题是... 有没有一个像样的Role Provider替代方案?(我知道我可以自定义Role provider、membership provider等)

所谓更加易管理/灵活,指的是我受限于使用Roles静态类,而不是直接实现与数据库上下文交互的服务层,而是必须使用具有自己数据库上下文的Roles静态类,此外表名也很糟糕。

谢谢您提前。


@Matti Virkkunen - 没错,忘了那部分吧 :) - ebb
2
你能详细说明一下你所说的“更易管理/灵活”是什么意思吗?目前看来,你甚至不确定自己想要什么。 - Matti Virkkunen
2
我有同样的问题。依赖注入甚至无法在提供程序中注入服务层,因为提供程序在我的 DI 甚至有机会注入之前就被执行了。 - Shawn Mclean
@Shawn - 请看我的回复。它会让您轻松地将适当的身份验证/授权服务注入到其他服务中。 - TheCloudlessSky
7
+1 是因为说供应商笨手笨脚——它们感觉像是一个失败的黑客马拉松活动的结果。 - EBarr
显示剩余3条评论
5个回答

89

我和你处境一样 - 我一直讨厌RoleProviders。是的,如果你想为一个小型网站快速搭建起来,它们很好用,但是它们并不是非常现实。我一直发现的主要问题是它们将你直接绑定到了ASP.NET。

我在最近的项目中采取的方式是定义了一些接口,这些接口是服务层的一部分(注:我简化了这些接口,但你也可以轻松地增加它们):

public interface IAuthenticationService
{
    bool Login(string username, string password);
    void Logout(User user);
}

public interface IAuthorizationService
{
    bool Authorize(User user, Roles requiredRoles);
}

然后您的用户可以拥有一个名为Roles的枚举:
public enum Roles
{
    Accounting = 1,
    Scheduling = 2,
    Prescriptions = 4
    // What ever else you need to define here.
    // Notice all powers of 2 so we can OR them to combine role permissions.
}

public class User
{
    bool IsAdministrator { get; set; }
    Roles Permissions { get; set; }
}

对于您的IAuthenticationService,您可以拥有一个基本实现来进行标准密码检查,然后您可以拥有一个FormsAuthenticationService,它会做更多的工作,例如设置Cookie等。对于您的AuthorizationService,您需要像这样的内容:

public class AuthorizationService : IAuthorizationService
{
    public bool Authorize(User userSession, Roles requiredRoles)
    {
        if (userSession.IsAdministrator)
        {
            return true;
        }
        else
        {
            // Check if the roles enum has the specific role bit set.
            return (requiredRoles & user.Roles) == requiredRoles;
        }
    }
}

除了这些基本服务,您可以轻松地添加重置密码等服务。

由于您正在使用MVC,因此可以使用ActionFilter在操作级别进行授权:

public class RequirePermissionFilter : IAuthorizationFilter
{
    private readonly IAuthorizationService authorizationService;
    private readonly Roles permissions;

    public RequirePermissionFilter(IAuthorizationService authorizationService, Roles requiredRoles)
    {
        this.authorizationService = authorizationService;
        this.permissions = requiredRoles;
        this.isAdministrator = isAdministrator;
    }

    private IAuthorizationService CreateAuthorizationService(HttpContextBase httpContext)
    {
        return this.authorizationService ?? new FormsAuthorizationService(httpContext);
    }

    public void OnAuthorization(AuthorizationContext filterContext)
    {
        var authSvc = this.CreateAuthorizationService(filterContext.HttpContext);
        // Get the current user... you could store in session or the HttpContext if you want too. It would be set inside the FormsAuthenticationService.
        var userSession = (User)filterContext.HttpContext.Session["CurrentUser"];

        var success = authSvc.Authorize(userSession, this.permissions);

        if (success)
        {
            // Since authorization is performed 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 or not a page should be served from the cache.
            var cache = filterContext.HttpContext.Response.Cache;
            cache.SetProxyMaxAge(new TimeSpan(0));
            cache.AddValidationCallback((HttpContext context, object data, ref HttpValidationStatus validationStatus) =>
            {
                validationStatus = this.OnCacheAuthorization(new HttpContextWrapper(context));
            }, null);
        }
        else
        {
            this.HandleUnauthorizedRequest(filterContext);
        }
    }

    private void HandleUnauthorizedRequest(AuthorizationContext filterContext)
    {
        // Ajax requests will return status code 500 because we don't want to return the result of the
        // redirect to the login page.
        if (filterContext.RequestContext.HttpContext.Request.IsAjaxRequest())
        {
            filterContext.Result = new HttpStatusCodeResult(500);
        }
        else
        {
            filterContext.Result = new HttpUnauthorizedResult();
        }
    }

    public HttpValidationStatus OnCacheAuthorization(HttpContextBase httpContext)
    {
        var authSvc = this.CreateAuthorizationService(httpContext);
        var userSession = (User)httpContext.Session["CurrentUser"];

        var success = authSvc.Authorize(userSession, this.permissions);

        if (success)
        {
            return HttpValidationStatus.Valid;
        }
        else
        {
            return HttpValidationStatus.IgnoreThisRequest;
        }
    }
}

然后您可以在控制器操作上进行装饰:

[RequirePermission(Roles.Accounting)]
public ViewResult Index()
{
   // ...
}

这种方法的优势在于您可以使用依赖注入和IoC容器进行连接。此外,您可以在多个应用程序中使用它(而不仅仅是您的ASP.NET应用程序)。您将使用ORM来定义适当的模式。
如果您需要有关FormsAuthorization / Authentication服务或接下来该怎么做的更多详细信息,请告诉我。
编辑:要添加“安全修剪”,您可以使用HtmlHelper完成。这可能需要更多的内容……但您已经了解到思路了。
public static bool SecurityTrim<TModel>(this HtmlHelper<TModel> source, Roles requiredRoles)
{
    var authorizationService = new FormsAuthorizationService();
    var user = (User)HttpContext.Current.Session["CurrentUser"];
    return authorizationService.Authorize(user, requiredRoles);
}

然后在你的视图中(这里使用Razor语法):

@if(Html.SecurityTrim(Roles.Accounting))
{
    <span>Only for accounting</span>
}

编辑: UserSession 的示例代码如下:

public class UserSession
{
    public int UserId { get; set; }
    public string UserName { get; set; }
    public bool IsAdministrator { get; set; }
    public Roles GetRoles()
    {
         // make the call to the database or whatever here.
         // or just turn this into a property.
    }
}

通过这种方式,我们不会在当前用户的会话中暴露密码散列和其他细节,因为它们实际上对于用户会话的生命周期并不是必要的。


3
完美无缺!只是一个好奇的问题:您如何检查视图中的用户角色?(为普通用户和管理员呈现不同的菜单项) - ebb
1
@ebb - 是的,这是一个罕见但有效的情况。你可以通知用户“权限更改将在用户再次登录后生效”,或者每次授权时始终加载权限(尽管会对数据库造成更多的访问)。 - TheCloudlessSky
1
@ebb - 在您的身份验证服务内部,您将从工作单元/存储库中获取用户。对我来说,在会话中存储实际用户实体感觉不对,因此我将其转换为UserSession(其中不保留密码等)。它只知道它需要知道的内容。因此,在您看到Session["CurrentUser"]时,您将设置/获取一个UserSession而不是User。请参见我的上面的编辑。有意义吗? - TheCloudlessSky
1
@DanielHarvey - 我会在视图的顶部做像 @using Namespace.To.Roles 这样的事情,或者将整个命名空间引用到角色中 @NS.To.Security.Roles.Accounting - TheCloudlessSky
1
@Ryan - 抱歉回复晚了 - 我没有看到你的回复。实际上,你不会在控制器代码中重新加载 - 你会在授权属性内部执行。在它加载之前,你可以检查会话是否已经过期,如果是,则重新查询数据库。因此,你只需要将它放在一个地方。 - TheCloudlessSky
显示剩余25条评论

5
我已经根据@TheCloudlessSky的帖子实现了一个基于角色的角色提供程序。我认为还有一些可以添加和分享的东西。 首先,如果您想将RequirepPermission类用作属性的操作过滤器,您需要为RequirepPermission类实现ActionFilterAttribute类。
接口类IAuthenticationServiceIAuthorizationService
public interface IAuthenticationService
{
    void SignIn(string userName, bool createPersistentCookie);
    void SignOut();
}

public interface IAuthorizationService
{
    bool Authorize(UserSession user, string[] requiredRoles);
}

FormsAuthenticationService class

/// <summary>
/// This class is for Form Authentication
/// </summary>
public class FormsAuthenticationService : IAuthenticationService
{

    public void SignIn(string userName, bool createPersistentCookie)
    {
        if (String.IsNullOrEmpty(userName)) throw new ArgumentException(@"Value cannot be null or empty.", "userName");

        FormsAuthentication.SetAuthCookie(userName, createPersistentCookie);
    }

    public void SignOut()
    {
        FormsAuthentication.SignOut();
    }
}

UserSession

public class UserSession
{
    public string UserName { get; set; }
    public IEnumerable<string> UserRoles { get; set; }
}

另一个要点是FormsAuthorizationService类以及我们如何将用户分配给httpContext.Session["CurrentUser"]。在这种情况下,我的方法是创建一个新的userSession类实例,并直接将用户从httpContext.User.Identity.Name分配给userSession变量,正如您可以在FormsAuthorizationService类中看到的那样。

[AttributeUsageAttribute(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Constructor | AttributeTargets.Method, Inherited = false)]
public class RequirePermissionAttribute : ActionFilterAttribute, IAuthorizationFilter
{
    #region Fields

    private readonly IAuthorizationService _authorizationService;
    private readonly string[] _permissions;

    #endregion

    #region Constructors

    public RequirePermissionAttribute(string requiredRoles)
    {
        _permissions = requiredRoles.Trim().Split(',').ToArray();
        _authorizationService = null;
    }

    #endregion

    #region Methods

    private IAuthorizationService CreateAuthorizationService(HttpContextBase httpContext)
    {
        return _authorizationService ?? new FormsAuthorizationService(httpContext);
    }

    public void OnAuthorization(AuthorizationContext filterContext)
    {
        var authSvc = CreateAuthorizationService(filterContext.HttpContext);
        // Get the current user... you could store in session or the HttpContext if you want too. It would be set inside the FormsAuthenticationService.
        if (filterContext.HttpContext.Session == null) return;
        if (filterContext.HttpContext.Request == null) return;
        var success = false;
        if (filterContext.HttpContext.Session["__Roles"] != null)
        {
            var rolesSession = filterContext.HttpContext.Session["__Roles"];
            var roles = rolesSession.ToString().Trim().Split(',').ToList();
            var userSession = new UserSession
            {
                UserName = filterContext.HttpContext.User.Identity.Name,
                UserRoles = roles
            };
            success = authSvc.Authorize(userSession, _permissions);
        }
        if (success)
            {
                // Since authorization is performed 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 or not a page should be served from the cache.
                var cache = filterContext.HttpContext.Response.Cache;
                cache.SetProxyMaxAge(new TimeSpan(0));
                cache.AddValidationCallback((HttpContext context, object data, ref HttpValidationStatus validationStatus) =>
                                                {
                                                    validationStatus = OnCacheAuthorization(new HttpContextWrapper(context));
                                                }, null);
            }
            else
            {
                HandleUnauthorizedRequest(filterContext);
            }
    }

    private static void HandleUnauthorizedRequest(AuthorizationContext filterContext)
    {
        // Ajax requests will return status code 500 because we don't want to return the result of the
        // redirect to the login page.
        if (filterContext.RequestContext.HttpContext.Request.IsAjaxRequest())
        {
            filterContext.Result = new HttpStatusCodeResult(500);
        }
        else
        {
            filterContext.Result = new HttpUnauthorizedResult();
        }
    }

    private HttpValidationStatus OnCacheAuthorization(HttpContextBase httpContext)
    {
        var authSvc = CreateAuthorizationService(httpContext);
        if (httpContext.Session != null)
        {
            var success = false;
            if (httpContext.Session["__Roles"] != null)
            {
                var rolesSession = httpContext.Session["__Roles"];
                var roles = rolesSession.ToString().Trim().Split(',').ToList();
                var userSession = new UserSession
                {
                    UserName = httpContext.User.Identity.Name,
                    UserRoles = roles
                };
                success = authSvc.Authorize(userSession, _permissions);
            }
            return success ? HttpValidationStatus.Valid : HttpValidationStatus.IgnoreThisRequest;
        }
        return 0;
    }

    #endregion
}

internal class FormsAuthorizationService : IAuthorizationService
{
    private readonly HttpContextBase _httpContext;

    public FormsAuthorizationService(HttpContextBase httpContext)
    {
        _httpContext = httpContext;
    }

    public bool Authorize(UserSession userSession, string[] requiredRoles)
    {
        return userSession.UserRoles.Any(role => requiredRoles.Any(item => item == role));
    }
}

在用户通过身份验证后,您可以从数据库中获取角色并将其分配给角色会话:

var roles = Repository.GetRolesByUserId(Id);
if (ControllerContext.HttpContext.Session != null)
   ControllerContext.HttpContext.Session.Add("__Roles",roles);
FormsService.SignIn(collection.Name, true);

用户退出系统后,您可以清除会话。
FormsService.SignOut();
Session.Abandon();
return RedirectToAction("Index", "Account");

这个模式的一个缺陷是,当用户登录系统并分配了一个角色时,授权不起作用,除非他退出并重新登录系统。

另一件事是不需要为角色单独创建一个类,因为我们可以直接从数据库获取角色并在控制器中将其设置为角色会话。

完成所有这些代码实现后,最后一步是将此属性绑定到控制器中的方法:

[RequirePermission("Admin,DM")]
public ActionResult Create()
{
return View();
}

2

0

你不需要使用静态类来管理角色。例如,SqlRoleProvider 允许你在数据库中定义角色。

当然,如果你想从自己的服务层检索角色,创建自己的角色提供程序并不难 - 实际上并没有太多方法需要实现。


1
@Matti Virkkunen - 我试图将角色提供程序和成员资格提供程序作为我的ORM映射的一部分,因为这将使我更加灵活。 - ebb
2
@ebb:你又变得含糊不清了。你具体想要做什么?你可以在你的提供程序中调用任何ORM方法。 - Matti Virkkunen
1
@ebb:我想你可以通过实现自己的RoleProvider来使IsInRole方法工作,因为它有一个IsUserInRole方法供你重写。 - Matti Virkkunen
@ebb:你还是没有解释清楚“太笨重”是什么意思。 - Matti Virkkunen
1
@Matti Virkkunen,绑定到一个奇怪名称的表中,你还需要在web.config中定义更多的内容来启用roleprovider,看起来你只能使用RoleProviders..所以这是列表中的另一个。但正如@TheCloudlessSky所提到的,我可以实现一个自定义提供程序,它仅包含IsUserInRole()方法的逻辑,然后只需为其余部分抛出NotImplemented Exceptions...但这很奇怪。 - ebb
显示剩余3条评论

0
您可以通过覆盖相应的接口来实现自己的成员资格角色提供程序。
如果您想从头开始,通常这些类型的东西是作为自定义Http模块实现的,它将用户凭据存储在HttpContext或会话中。 无论哪种方式,您都可能需要设置带有某种身份验证令牌的cookie。

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