坚持创建一个“安全修剪”的html.ActionLink扩展方法

时间:2008-09-23 13:30:41

标签: asp.net-mvc

我正在尝试为MVC的htmlHelper创建一个扩展方法。 目的是基于控制器/操作上设置的AuthorizeAttribute启用或禁用ActionLink。 借用MVCSitemap Maarten Balliauw创建的代码,我想在决定如何呈现actionlink之前验证用户对控制器/操作的权限。 当我尝试获取MvcHandler时,我得到一个空值。 是否有更好的方法来控制器/操作的属性?

以下是扩展方法的代码:

public static class HtmlHelperExtensions
{
    public static string SecurityTrimmedActionLink(this HtmlHelper htmlHelper, string linkText, string action, string controller)
    {
        //simplified for brevity 
        if (IsAccessibleToUser(action, controller))
        {
            return htmlHelper.ActionLink(linkText, action,controller);    
        }
        else
        {
            return String.Format("<span>{0}</span>",linkText);    
        }
    }

    public static bool IsAccessibleToUser(string action, string controller)
    {
        HttpContext context = HttpContext.Current;

        MvcHandler handler = context.Handler as MvcHandler;            

        IController verifyController = 
            ControllerBuilder
            .Current
            .GetControllerFactory()
            .CreateController(handler.RequestContext, controller);

        object[] controllerAttributes = verifyController.GetType().GetCustomAttributes(typeof(AuthorizeAttribute), true);
        object[] actionAttributes = verifyController.GetType().GetMethod(action).GetCustomAttributes(typeof(AuthorizeAttribute), true);

        if (controllerAttributes.Length == 0 && actionAttributes.Length == 0)
            return true;

        IPrincipal principal = handler.RequestContext.HttpContext.User;

        string roles = "";
        string users = "";
        if (controllerAttributes.Length > 0)
        {
            AuthorizeAttribute attribute = controllerAttributes[0] as AuthorizeAttribute;
            roles += attribute.Roles;
            users += attribute.Users;
        }
        if (actionAttributes.Length > 0)
        {
            AuthorizeAttribute attribute = actionAttributes[0] as AuthorizeAttribute;
            roles += attribute.Roles;
            users += attribute.Users;
        }

        if (string.IsNullOrEmpty(roles) && string.IsNullOrEmpty(users) && principal.Identity.IsAuthenticated)
            return true;

        string[] roleArray = roles.Split(',');
        string[] usersArray = users.Split(',');
        foreach (string role in roleArray)
        {
            if (role != "*" && !principal.IsInRole(role)) return false;
        }
        foreach (string user in usersArray)
        {
            if (user != "*" && (principal.Identity.Name == "" || principal.Identity.Name != user)) return false;
        }
        return true;
    }

}

3 个答案:

答案 0 :(得分:4)

以下是工作代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Security.Principal;
using System.Web.Routing;
using System.Web.Mvc;
using System.Collections;
using System.Reflection;
namespace System.Web.Mvc.Html
{
    public static class HtmlHelperExtensions
    {
        public static string SecurityTrimmedActionLink(
        this HtmlHelper htmlHelper,
        string linkText,
        string action,
        string controller)
        {
            return SecurityTrimmedActionLink(htmlHelper, linkText, action, controller, false);
        }
        public static string SecurityTrimmedActionLink(this HtmlHelper htmlHelper, string linkText, string action, string controller, bool showDisabled)
        {
            if (IsAccessibleToUser(action, controller))
            {
                return htmlHelper.ActionLink(linkText, action, controller);
            }
            else
            {
                return showDisabled ? String.Format("<span>{0}</span>", linkText) : "";
            }
        }
        public static bool IsAccessibleToUser(string actionAuthorize, string controllerAuthorize)
        {
            Assembly assembly = Assembly.GetExecutingAssembly();
            GetControllerType(controllerAuthorize);
            Type controllerType = GetControllerType(controllerAuthorize);
            var controller = (IController)Activator.CreateInstance(controllerType);
            ArrayList controllerAttributes = new ArrayList(controller.GetType().GetCustomAttributes(typeof(AuthorizeAttribute), true));
            ArrayList actionAttributes = new ArrayList();
            MethodInfo[] methods = controller.GetType().GetMethods();
            foreach (MethodInfo method in methods)
            {
                object[] attributes = method.GetCustomAttributes(typeof(ActionNameAttribute), true);
                if ((attributes.Length == 0 && method.Name == actionAuthorize) || (attributes.Length > 0 && ((ActionNameAttribute)attributes[0]).Name == actionAuthorize))
                {
                    actionAttributes.AddRange(method.GetCustomAttributes(typeof(AuthorizeAttribute), true));
                }
            }
            if (controllerAttributes.Count == 0 && actionAttributes.Count == 0)
                return true;

            IPrincipal principal = HttpContext.Current.User;
            string roles = "";
            string users = "";
            if (controllerAttributes.Count > 0)
            {
                AuthorizeAttribute attribute = controllerAttributes[0] as AuthorizeAttribute;
                roles += attribute.Roles;
                users += attribute.Users;
            }
            if (actionAttributes.Count > 0)
            {
                AuthorizeAttribute attribute = actionAttributes[0] as AuthorizeAttribute;
                roles += attribute.Roles;
                users += attribute.Users;
            }

            if (string.IsNullOrEmpty(roles) && string.IsNullOrEmpty(users) && principal.Identity.IsAuthenticated)
                return true;

            string[] roleArray = roles.Split(',');
            string[] usersArray = users.Split(',');
            foreach (string role in roleArray)
            {
                if (role == "*" || principal.IsInRole(role))
                    return true;
            }
            foreach (string user in usersArray)
            {
                if (user == "*" && (principal.Identity.Name == user))
                    return true;
            }
            return false;
        }

        public static Type GetControllerType(string controllerName)
        {
            Assembly assembly = Assembly.GetExecutingAssembly();
            foreach (Type type in assembly.GetTypes())
            {
                if (type.BaseType.Name == "Controller" && (type.Name.ToUpper() == (controllerName.ToUpper() + "Controller".ToUpper())))
                {
                    return type;
                }
            }
            return null;
        }
    }
}

我不喜欢使用反射,但我无法访问ControllerTypeCache。

答案 1 :(得分:0)

您的ViewPage引用了视图上下文,因此您可以将其作为扩展方法。

然后你可以说,如果Request.IsAuthenticated或Request.User.IsInRole(...)

用法就像<%= this.SecurityLink(text, demandRole, controller, action, values) %>

答案 2 :(得分:0)

我真的很喜欢@ Robert的帖子中的代码,但是有一些错误,我想缓存角色和用户的聚集,因为反思可能需要花费一些时间。

修复了错误:如果同时存在Controller属性和Action属性,那么当角色连接在一起时,控制器角色和动作角色之间不会插入额外的逗号,而这些角色将无法正确分析。

[Authorize(Roles = "SuperAdmin,Executives")]
public class SomeController() {
    [Authorize(Roles = "Accounting")]    
    public ActionResult Stuff() {
    }
}

然后角色字符串最终为SuperAdmin,ExecutivesAccounting,我的版本确保了高管和会计是分开的。

我的新代码也忽略了HttpPost操作上的Auth,因为这可能会让事情失败,尽管不太可能。

最后,对于较新版本的MVC,它会返回MvcHtmlString而不是string

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Reflection;
using System.Collections;
using System.Web.Mvc;
using System.Web.Mvc.Html;
using System.Security.Principal;


public static class HtmlHelperExtensions
{
    /// <summary>
    /// only show links the user has access to
    /// </summary>
    /// <returns></returns>
    public static MvcHtmlString SecurityLink(this HtmlHelper htmlHelper, string linkText, string action, string controller, bool showDisabled = false)
    {
        if (IsAccessibleToUser(action, controller))
        {
            return htmlHelper.ActionLink(linkText, action, controller);
        }
        else
        {
            return new MvcHtmlString(showDisabled ? String.Format("<span>{0}</span>", linkText) : "");
        }
    }

    /// <summary>
    /// reflection can be kinda slow, lets cache auth info
    /// </summary>
    private static Dictionary<string, Tuple<string[], string[]>> _controllerAndActionToRolesAndUsers = new Dictionary<string, Tuple<string[], string[]>>();


    private static Tuple<string[], string[]> GetAuthRolesAndUsers(string actionName, string controllerName)
    {
        var controllerAndAction = controllerName + "~~" + actionName;
        if (_controllerAndActionToRolesAndUsers.ContainsKey(controllerAndAction))
            return _controllerAndActionToRolesAndUsers[controllerAndAction];

        Type controllerType = GetControllerType(controllerName);
        MethodInfo matchingMethodInfo = null;

        foreach (MethodInfo method in controllerType.GetMethods())
        {
            if (method.GetCustomAttributes(typeof(HttpPostAttribute), true).Any())
                continue;
            if (method.GetCustomAttributes(typeof(HttpPutAttribute), true).Any())
                continue;
            if (method.GetCustomAttributes(typeof(HttpDeleteAttribute), true).Any())
                continue;

            var actionNameAttr = method.GetCustomAttributes(typeof(ActionNameAttribute), true).Cast<ActionNameAttribute>().FirstOrDefault();
            if ((actionNameAttr == null && method.Name == actionName) || (actionNameAttr != null && actionNameAttr.Name == actionName))
            {
                matchingMethodInfo = method;
            }
        }

        if (matchingMethodInfo == null)
            return new Tuple<string[], string[]>(new string[0], new string[0]);

        var authAttrs = new List<AuthorizeAttribute>();
        authAttrs.AddRange(controllerType.GetCustomAttributes(typeof(AuthorizeAttribute), true).Cast<AuthorizeAttribute>());

        var roles = new List<string>();
        var users = new List<string>();

        foreach(var authAttr in authAttrs)
        {
            roles.AddRange(authAttr.Roles.Split(','));
            users.AddRange(authAttr.Roles.Split(','));
        }

        var rolesAndUsers = new Tuple<string[], string[]>(roles.ToArray(), users.ToArray());
        try
        {
            _controllerAndActionToRolesAndUsers.Add(controllerAndAction, rolesAndUsers);
        }
        catch (System.ArgumentException ex)
        {
            //possible but unlikely that two threads hit this code at the exact same time and enter a race condition
            //instead of using a mutex, we'll just swallow the exception when the method gets added to dictionary 
            //for the second time. mutex only allow single worker regardless of which action method they're getting
            //auth for. doing it this way eliminates permanent bottleneck in favor of a once in a bluemoon time hit
        }

        return rolesAndUsers;
    }

    public static bool IsAccessibleToUser(string actionName, string controllerName)
    {
        var rolesAndUsers = GetAuthRolesAndUsers(actionName, controllerName);
        var roles = rolesAndUsers.Item1;
        var users = rolesAndUsers.Item2;

        IPrincipal principal = HttpContext.Current.User;

        if (!roles.Any() && !users.Any() && principal.Identity.IsAuthenticated)
            return true;


        foreach (string role in roles)
        {
            if (role == "*" || principal.IsInRole(role))
                return true;
        }
        foreach (string user in users)
        {
            if (user == "*" && (principal.Identity.Name == user))
                return true;
        }

        return false;
    }

    public static Type GetControllerType(string controllerName)
    {
        Assembly assembly = Assembly.GetExecutingAssembly();
        foreach (Type type in assembly.GetTypes())
        {
            if (type.BaseType.Name == "Controller" && (type.Name.ToUpper() == (controllerName.ToUpper() + "Controller".ToUpper())))
            {
                return type;
            }
        }
        return null;
    }


}