ASP.NET MVC - 角色提供者的替代方案?

ebb*_*ebb 46 asp.net authentication asp.net-mvc authorization asp.net-membership

我试图避免使用角色提供者和成员资格提供者,因为在我看来它过于笨拙,因此我正在尝试制作我自己的"版本",它不那么笨拙,更易于管理/灵活.现在是我的问题..角色提供者有替代方案吗?(我知道我可以做自定义角色提供者,会员提供者等)

通过更易于管理/灵活,我的意思是我只能使用Roles静态类而不是直接实现到与数据库上下文交互的服务层,而是我必须使用具有自己的数据库上下文的Roles静态类等,表名也很糟糕..

提前致谢.

The*_*Sky 87

我和你在同一条船上 - 我一直都很讨厌角色提供者.是的,如果你想为一个小网站搞定并运行它们,它们就很棒,但它们并不是很现实.我一直发现的主要缺点是它们直接将你绑定到ASP.NET.

我最近的一个项目的方式是定义一些作为服务层一部分的接口(注意:我简化了一些 - 但你可以很容易地添加它们):

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

public interface IAuthorizationService
{
    bool Authorize(User user, Roles requiredRoles);
}
Run Code Online (Sandbox Code Playgroud)

然后你的用户可以有一个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; }
}
Run Code Online (Sandbox Code Playgroud)

对于你来说IAuthenticationService,你可以有一个基本的实现,它可以进行标准的密码检查,然后你可以FormsAuthenticationService做一些更多的AuthorizationService事情,例如设置cookie等.对于你,你需要这样的东西:

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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

除了这些基本服务之外,您还可以轻松添加服务以重置密码等.

由于您使用的是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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

[RequirePermission(Roles.Accounting)]
public ViewResult Index()
{
   // ...
}
Run Code Online (Sandbox Code Playgroud)

这种方法的优点是您还可以使用依赖注入和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);
}
Run Code Online (Sandbox Code Playgroud)

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

@if(Html.SecurityTrim(Roles.Accounting))
{
    <span>Only for accounting</span>
}
Run Code Online (Sandbox Code Playgroud)

编辑: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.
    }
}
Run Code Online (Sandbox Code Playgroud)

这样,我们不会在当前用户的会话中公开密码哈希和所有其他详细信息,因为用户的会话生存期实际上并不需要它们.

  • 没有什么比完美!只是一个奇怪的问题:您如何检查用户是否在视图中扮演角色?(为常规用户和管理员呈现不同的菜单项)? (3认同)

Ham*_*oli 5

我在这里发布了基于@TheCloudlessSky的角色提供程序.我认为我可以添加和分享我所做的一些事情.首先,如果要将RequirepPermission动作过滤器的类用作属性,则需要为ActionFilterAttribute类实现RequirepPermission类.

接口类IAuthenticationServiceIAuthorizationService

public interface IAuthenticationService
{
    void SignIn(string userName, bool createPersistentCookie);
    void SignOut();
}

public interface IAuthorizationService
{
    bool Authorize(UserSession user, string[] requiredRoles);
}
Run Code Online (Sandbox Code Playgroud)

FormsAuthenticationService

/// <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();
    }
}
Run Code Online (Sandbox Code Playgroud)

UserSession CALSS

public class UserSession
{
    public string UserName { get; set; }
    public IEnumerable<string> UserRoles { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

另一点是FormsAuthorizationService类以及我们如何将用户分配给httpContext.Session["CurrentUser"].我在这种情况下的方法是创建一个新的userSession类实例,并直接将用户分配给httpContext.User.Identity.NameuserSession变量,如同在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));
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

用户退出系统后,您可以清除会话

FormsService.SignOut();
Session.Abandon();
return RedirectToAction("Index", "Account");
Run Code Online (Sandbox Code Playgroud)

此模型中的警告是,当用户登录到系统时,如果将角色分配给用户,除非他注销并重新登录系统,否则授权不起作用.

另一件事是没有必要为角色设置单独的类,因为我们可以直接从数据库获取角色并将其设置为控制器中的角色会话.

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

[RequirePermission("Admin,DM")]
public ActionResult Create()
{
return View();
}
Run Code Online (Sandbox Code Playgroud)