从model / ViewModel中访问身份验证/授权信息的最佳实践?

时间:2011-12-01 11:52:48

标签: asp.net-mvc

所以我买了“瘦控制器,胖模型”指南。这对我来说意味着控制器中尽可能少的代码,并且大多数/所有实际业务逻辑都在模型中(或在单独的存储库/服务代码中)。

实际上,我喜欢控制器是视图和模型之间非常简单的管道的想法,并且主要关注在模型中调用适当的方法来执行某些操作,捕获异常然后添加为ModelErrors,以及决定下一步的观点。保持尽可能简单。

至少我买了所有这些,直到我试图在我的模型中编写一些代码来处理与身份和角色相关的任何代码。

似乎所有需要的信息都在Controller基类中。因此,我可以想到在Model方法中真正访问它的唯一方法是将其作为参数传递?真的很快变得非常难看。

如何从模型中访问信息(IPrincipal,会话信息等)?

1 个答案:

答案 0 :(得分:2)

我通过创建一个基本控制器(并从中继承每个控制器)和一个基本模型(强制我的应用程序中的每个模型继承)来解决这个问题。

在2个基础对象中,我添加了2个属性

public string UserId { get; private set; }
public Object UserInfo { get; private set; }

包含只读用户信息(userId也是会话存储,但对于UserInfo,我使用了MemoryCache)。

通过这种方式,我可以从我的控制器和模型中访问所有内容(即使在视图中也是如此)。

信息仅在基本控制器中设置(通过覆盖OnAuthentication或使用自定义AuthorizeAttribute)。

我不知道这是否在政治上是正确的,但由于我不受ASP.NET编写的约束(必须使用Kerberos风格的SSO),我选择以这种方式实现。

编辑:我是怎么做到的:

我有一个HomeController,它只是将userId存储在会话中,然后转发到webapp的真正起点(一个扩展我BaseController的控制器)

通过在基本控制器上使用Authorize属性来保护此模式,每个派生控制器都会为每个操作应用它,因此基本控制器的OnAuthorization方法(如果需要覆盖,则为派生控制器中的一个)特殊情况)将被解雇(例如,如果没有从HomeController / Index()传递至少一次就无法访问)

模拟基本控制器:

[Authorize]
[HandleError]
public abstract class BaseController : Controller
{
    public string UserId
    {
        get
        {
            return (string)HttpContext.Session[MyConstants.UserId];
        }
    }
    public Object UserInfo
    {
        get { /* Access a MemoryCache with the UserId and SessionId */ }
    }


    [NonAction]
    protected override void OnAuthorization(AuthorizationContext filterContext)
    {
        String Reason = "Everything's good :D";
        bool Ok = true;
        bool auth = true;
        // Base authorization (NTLM)
        base.OnAuthorization(filterContext);

        // Checks retriving UserId from session and base authentication
        Ok = ((UserId != null) && // Exits in session
              (filterContext.HttpContext.User.Identity.IsAuthenticated) // is legit
         );
        if (!Ok) {
            Reason = "Meaningfull message (session expired or not authenticated)!";
        } else {
            // My Authorization Tests Start here
            try
            {
                MyUserInfo u = (MyUserInfo) UserInfo; 
                if (u != null) 
                {
                    // Found, check if has rights to access (heavy business logic in the IsAuthorized omitted)
                    Ok = u.IsAuthorized();
                }

                if (!Ok)
                {
                    Reason = "Your credentials don't allow you to do this!";
                }
            }
            catch (Exception e)
            {
                Reason = "doooh, exception checking auth: " + e.Message ;
                Ok = false;
            }
        }

        if (!Ok) {
            if (Request.IsAjaxRequest())
            {
                // If it was an ajax, I return a status 418, via global Ajax Error function I handle this
                // client side with an alert.
                filterContext.Result = new HttpStatusCodeResultWithJson(418, Reason); 
            }
            else
            {
                // Redirect to a specific Controller
                TempData["Reason"] = Reason;
                // A class which uses the Url helper in order to build up an url
                string denied = UrlFactory.GetUrl("Denied", "Errors", null);
                // Redirect
                filterContext.Result = new RedirectResult(denied);
            }
        }
        return;
    }
}

当然你需要一个不从基础继承的HomeController(你的webapp的入口点)

    public class HomeController : Controller 
    {
    [HttpGet]
    public ActionResult Index(string StringaRandom, string HashCalcolato)
    {
        string Motivo = "";
        string UserId = null;
        try {
            bool UtenteCollegato = MySignOn(StringaRandom, HashCalcolato, ref UserId, ref  Motivo);
            // ok, valid user, SignOn stores UserInfos in a Cache (SLQ Server or MemoryCache)
            if (UtenteCollegato)
            {
                if (HttpContext.Session != null)
                {
                    // Salvo in sessione
                    HttpContext.Session.Add(MyConstants.UserId, UserId);
                }
                // Redirect to the start controller (which inherits from BaseController)
                return RedirectToAction("Index", "Start");
            }
        }
        catch (Exception e)
        {
            Log.Error(e);  
            Motivo = "Errore interno: " + e.Message;
        }
        HttpContext.Session.Remove(MyConstants.UserId);
        string denied = UrlFactory.GetUrl("Denied", "Errors", null);
        TempData["Reason"] = Motivo;
        return new RedirectResult(denied);
    }
  }

StartController继承自BaseController,因此每个动作之前需要OnAuthorize调用,因为StartController不会覆盖它,所以调用BaseController。

或多或少这就是一切,作为奖励我添加了HttpStatusCodeWithJon类。

public class HttpStatusCodeResultWithJson : JsonResult
{
    private int _statusCode;
    private string _description;
    public HttpStatusCodeResultWithJson(int statusCode, string description = null)
    {
        _statusCode = statusCode;
        _description = description;
    }
    public override void ExecuteResult(ControllerContext context)
    {
        var httpContext = context.HttpContext;
        var response = httpContext.Response;
        response.StatusCode = _statusCode;
        response.StatusDescription = _description;
        base.JsonRequestBehavior = JsonRequestBehavior.AllowGet;
        base.ExecuteResult(context);
    }
}

此类可用于触发Ajax回调的STATUS错误。如果你使用jQuery,那么你可以使用全局ajax错误函数来管理它。

就是这样,也许它不优雅,也许在政治上是不正确的,但几乎我所需要的一切,它有点集中,并且对我目前的项目有效。

对于模型,只需为userid和userInfo添加一个公共get,private set属性,并在模型构造函数中设置它们(因为每个模型都是在控制器中创建的,所以在调用基本模型构造函数时应该没有任何问题: base(params)

警告:代码是真实的模拟(因此可能有拼写错误或遗漏的东西),我已经避免粘贴我的业务逻辑并重新设计了一些部分,我想它可以被视为一个好的手绘路线图

如果这有帮助或者您需要其他信息,请告诉我。

PS:我差点忘了。如果您使用ASP.NET配置文件,身份,我建议您查看AuthorizeAttribute类,您可以扩展它并创建自己的授权属性,在这种情况下,您不需要在基本控制器上编写OnAuthorization或有继承(我仍然建议你有一个基本模型和一个基本控制器),但你将在你的属性中提供该方法。它更清洁。我没有这样做是因为我的Single Sign On解决方案存在一些遗留约束,但是会迁移到那个。

模型中的自动注入可以扩展ModelBinder(或注册自定义的)。从来没有深入研究,我更喜欢另一种数据过滤方法(它的授权不是身份验证,对我来说是基于应用程序的,不能依赖ASP.NET分析)

我可能会使用的方法是让业务对象处理DataFiltering

假设你有像

这样的动作
ActionResult Something(SomeModel TheModel) {
  // perform anything
  TheModel.DoSomething();
  return View(TheModel);
}

您可以将其更改为

ActionResult Something(SomeModel TheModel) 
{
   MyBusiness bsn = new MyNusiness(UserId, TheModel); // Give UserId or UserInfo directly to business
   TheModel = bsn.SomethingInABusinessWay();
   return View(TheModel);
}

或者如果要保留模型中的所有内容,只需将UserId参数添加到DoSomething方法即可。是的,我们正在处理对象,但是在某些情况下,对象也可以依赖外部数据(不仅仅是数据成员或属性)。

这是一个非常简洁快速的解决方案,主要缺点是为每个vm业务方法添加一个参数,但它比扫描每个动作以便注入它更好(至少编译器在每次调用时都会出错)< / p>

如果我从一些javascript命名空间中解脱出来,我会更进一步注意扩展默认模型绑定器的模型中的sproperty。但是如果我没记错的话,我已经看到了类似的东西。 net(Phil Haak或ScottGu的博客,甚至是SO),只是在运行时搜索模型中的注入数据。