Obtaining the current Principal outside of the Web

2019-04-23 17:22发布

问题:

I have the following ntier app: MVC > Services > Repository > Domain. I am using Forms authentication. Is it safe to use Thread.CurrentPrincipal outside of my MVC layer to get the currently logged in user of my application or should I be using HttpContext.Current.User?

The reason I ask is there seems to be some issues around Thread.CurrentPrincipal, but I am cautious to add a reference to System.Web outside of my MVC layer in case I need to provide a non web font end in the future.

Update

I have been following the advice recieved so far to pass the username into the Service as part of the params to the method being called and this has lead to a refinement of my original question. I need to be able to check if the user is in a particular role in a number of my Service and Domain methods. There seems to be a couple of solutions to this, just wondering which is the best way to proceed:

  1. Pass the whole HttpContext.Current.User as a param instead of just the username.
  2. Call Thread.CurrentPrincipal outside of my web tier and use that. But how do I ensure it is equal to HttpContext.Current.User?
  3. Stick to passing in the username as suggested so far and then use Roles.IsUserInRole. The problem with this approach is that it requires a ref to System.Web which I feel is not correct outside of my MVC layer.

How would you suggest I proceed?

回答1:

I wouldn't do either, HttpContext.Current.User is specific to your web layer.

Why not inject the username into your service layer?



回答2:

Map the relevant User details to a new Class to represent the LoggedInUser and pass that as an argument to your Business layer method

 public class LoggedInUser
 {
   public string UserName { set;get;}
   //other relevant proerties
 }

Now set the values of this and pass to your BL method

var usr=new LoggedInUser();
usr.UserName="test value ";  //Read from the FormsAuthentication stuff and Set
var result=YourBusinessLayerClass.SomeOperation(usr);


回答3:

You should abstract your user information so that it doesn't depend on Thread.CurrentPrincipal or HttpContext.Current.User.

You could add a constructor or method parameter that accepts a user name, for example.

Here's an overly simplified example of a constructor parameter:

class YourBusinessClass 
{
   string _userName;
   public YourBusinessClass(string userName)
   {
      _userName = userName;
   }

   public void SomeBusinessMethodThatNeedsUserName()
   {
      if (_userName == "sally")
      {
         // do something for sally
      }
   }
}


回答4:

I prefer option number 2( use Thread.CurrentPrincipal outside of web tier ). since this will not polute your service tier & data tier methods. with bonuses: you can store your roles + additional info in the custom principal;

To make sure Thread.CurrentPrincipal in your service and data tier is the same as your web tier; you can set your HttpContext.Current.User (Context.User) in Global.asax(Application_AuthenticateRequest). Other alternative location where you can set this are added at the bottom.

sample code:

    //sample synchronizing HttpContext.Current.User with Thread.CurrentPrincipal
    protected void Application_AuthenticateRequest(Object sender, EventArgs e)
    {
        HttpCookie authCookie = Request.Cookies[FormsAuthentication.FormsCookieName];

        //make sure principal is not set for anonymous user/unauthenticated request
        if (authCookie != null && Request.IsAuthenticated)
        {
            FormsAuthenticationTicket authTicket = FormsAuthentication.Decrypt(authCookie.Value);

            //your additional info stored in cookies: multiple roles, privileges, etc
            string userData = authTicket.UserData;

            CustomPrincipal userPrincipal = PrincipalHelper.CreatePrincipal(authTicket.Name, authTicket.UserData, Request.IsAuthenticated);

            Context.User = userPrincipal;
        }
    }

of course first you must implement your login form to create authorization cookies containing your custom principal.

Application_AuthenticateRequest will be executed for any request to server(css files, javascript files, images files etc). To limit this functionality only to controller action, you can try setting the custom principal in ActionFilter(I haven't tried this). What I have tried is setting this functionality inside an Interceptor for Controllers(I use Castle Windsor for my Dependency Injection and Aspect Oriented Programming).



回答5:

I believe you are running into this problem because you need to limit your domains responsibility further. It should not be the responsibility of your service or your document to handle authorization. That responsibility should be handled by your MVC layer, as the current user is logged in to your web app, not your domain.

If, instead of trying to look up the current user from your service, or document, you perform the check in your MVC app, you get something like this:

if(Roles.IsUserInRole("DocumentEditorRole")){

    //UpdateDocument does NOT authorize the user. It does only 1 thing, update the document.
    myDocumentService.UpdateDocument(currentUsername, documentToEdit);

} else {

    lblPermissionDenied.InnerText = @"You do not have permission 
                                      to edit this document.";

}

which is clean, easy to read, and allows you to keep your services and domain classes free from authorization concerns. You can still map Roles.IsUserInRole("DocumentEditorRole")to your viewmodel, so the only this you are losing, is the CurrentUserCanEdit method on your Document class. But if you think of your domain model as representing real world objects, that method doesn't belong on Document anyway. You might think of it as a method on a domain User object (user.CanEditDocument(doc)), but all in all, I think you will be happier if you keep your authorization out of your domain layer.