In this article, I’ll focus on the security capabilities of WCF RIA Services. Security is one of the areas where RIA Services took something that is vitally important to an application and can be very complex when using WCF on its own, and gave us a simple to use approach that covers 80% of the cases with minimal code and confusion, while still covering most of the remaining 20% nicely with the extensibility hooks in RIA Services for security. Like many other aspects of RIA Services, you are insulated from the service level plumbing that needs to happen to secure your communications and calls and just lets you focus on putting the checks and balances in place to make sure that only things that are supposed to happen in your application are allowed.
There are three concerns to focus on with service security: authentication, authorization, and transfer security. Authentication is simply the act of identifying the caller and determining if you believe they are who they say they are. Authorization is determining what you are going to allow them to do once you know who they are. And transfer security has to do with protecting the messages when they hit the wire so that someone cannot view or tamper with the messages enroute. In addition, you have client side security concerns in that you may not want anyone to be able to run your Silverlight application, and when they do, you want to know who they are both so you can make the service calls and to authorize different functionality on the client side. RIA Services has built-in functionality to cover all of these based on proven and standardized protocols and based on the pre-existing infrastructure of .NET for security.
On important thing to understand is that the security model of the service calls from your Silverlight client to your domain services is a separate thing from the security provided by the hosting web application. When your Silverlight application launches initially in the browser, the host web page is first accessed from the site. Then your XAP is downloaded as a file and launched on the client side. After it launches, the application can make RIA Service calls to the back end, and those calls first manifest themselves on the server side as HTTP requests for an svc file to the web server. This process is shown in the figure below.
The security of those calls is determined by the configuration of the hosting web site, which may demand Windows or Forms authentication itself to restrict access to the files. You could choose to rely entirely on site level security, but often you need to have more explicit points of control inside your client and service code where you need to know who the user is and what you are going to allow them to do. This is where RIA Services security steps in and gives you the control you need, both server and client side.
WCF RIA Services security allows you to:
The really quick way to start an application to use WCF RIA Services security is to use the Silverlight Business Application project template. That template sets up all the all the configuration and services stuff that I am going to walk through in this article by default. But I don’t think you should use any capability without understanding what it is doing for you, especially with something as important as security. So I am going to walk through the mechanisms to set it up manually so you understand what you need and what the different pieces do for you.
So lets dive right in. You can download the completed sample code for this article here.
The most common scenarios for authenticating the user include using either Windows credentials or using a username/password (username credentials). For the Windows case, you may require the user to enter a domain account on the client side because they could be working from a home or public computer that is not part of the domain, or you might leverage integrated security to have the hosting browser or application send the Windows credentials automatically. For the username case, you may be validating the username against a database, LDAP or other custom store. In both of these cases as long as you don’t need to do anything too fancy, it can be extremely simple to set up by letting the host web site and browser do the heavy lifting for you. You could choose to authenticate the user in an ASP.NET web page to establish a forms authentication session before you launch the Silverlight application. Doing so would let you secure access to the XAP file download as well as the svc file service calls just based on web site security without doing anything inside of your Silverlight app. Or you could allow unrestricted XAP file download and launch, and then authenticate the user after the Silverlight app launches. There are many options supported by Silverlight itself here, regardless of whether you are using WCF RIA Services or not.
On top of the basic security mechanisms of the host site and Silverlight, you may want to have specific points of enforcement in the service calls on the back end. Additionally, you often need to know who the authenticated user is on the client side, as well as what roles they are in, to modify the behavior of the client application appropriately (i.e. hide or disable commands that the user is not authorized to invoke). Natively in Silverlight, there is no security context, even if the application launch happens as part of a secure session in the browser. The threads do not have security principals on them like in a normal .NET application that could tell you who the authenticated user is. WCF RIA Services steps in here for us and provides the infrastructure to not only authenticate the calls on the server side, but to also return the authentication and authorization context to the client so that you can easily address these scenarios.
If you don’t need the client to know about the user and their roles, it is really simple to secure the back end with RIA Services. The first step is to turn on authentication in the web host site with either Windows or Forms as the mode.
<authentication mode="Forms"/>
Once you have done this, RIA Services can use the configured or default membership provider to look up the username/password that are sent from the client to authenticate them. If you choose to use Windows credentials, those credentials would be validated by IIS itself through the OS. You can collect the user credentials in either case before the Silverlight application launches through a web login form that uses normal Forms Authentication to pass the user credentials in a cookie, or by using RIA Services to pass the credentials from within the Silverlight application. There is nothing special about configuring membership or role providers with respect to RIA Services. That topic has been well covered in many other places in the context of ASP.NET security or Silverlight itself. For a good walkthrough of how to configure things, see the configuration section of this walkthrough.
For this article, I am going to assume you want to provide the login experience as part of the Silverlight application, and that you will want the user identity and roles available on the client. That means you will need to expose the XAP file from the hosting web site for unauthenticated download unless you want to force the user to log in twice. If instead you want to authenticate via a web page AND you do not need to do any client side authorization or user customization, then you could just rely entirely on the server configuration and services code. But the richer scenarios enabled by RIA Services is what I want to focus on.
To minimize the amount of configuration required to run the sample code, I am using a custom membership provider that is part of the web host project. That provider works against the User table that is part of the TaskManager database. Additionally, that provider just expects the passwords to be stored in the clear in the database so that you can use a predefined account in the database (Username=Brian, Password=IDesign) with no set up. Naturally this is not what you should do in production. With a minor modification to the membership provider, you can hash the incoming password and compare to a hashed version in the database. This requires you to have a mechanism for creating the user accounts that will use the same hashing algorithm when creating the user account. Again, there are lots of examples out there of doing this, so I won’t cover that here.
The custom membership provider just uses the same Entity Framework model to look up username/password combinations to authenticate the calls. The custom membership provider looks like this:
public class CustomMembershipProvider : MembershipProvider
{
public override bool ValidateUser(string username, string password)
{
using (TaskManagerEntities context = new TaskManagerEntities())
{
var user = context.Users.Where(u => u.Username == username &&
u.Password == password).FirstOrDefault();
return user != null;
}
}
public override string ApplicationName
{
get { return "TaskManager"; }
set { }
}
// Other overrides not implemented
...
}
At runtime, the only method in the provider that matters is the ValidateUser method. It will look up the credentials passed in and validate them. If valid, the membership provider and RIA Services will set up an authentication context on the server side. If you expose things right from your services, you can get that authentication context back in the client.
After you have defined your custom provider, or if you want to use the built-in SQL provider, you just need to configure the site for that provider:
<system.web>
<authentication mode="Forms" />
<membership defaultProvider="myCustomProvider">
<providers>
<add name="myCustomProvider" type="TaskManager.Web.CustomMembershipProvider,TaskManager.Web"/>
</providers>
</membership>
...
</system.web>
Now your site and RIA Services have enough information server side to authenticate the user with a username/password. However, you need a little more support client side to provide that information.
In order to have the right support on the client side to send the credentials and to establish a client side authentication context after successfully authenticating, you need to define an Authentication Domain Service as part of your host site. This is really a trivial matter unless you want to get into custom authentication scenarios because the RIA Services base classes provide you with everything you need for authenticating and authorizing through the membership and role providers.
To do this, just go to the host web site project, and add a new item to the project (right click on the project in Solution Explorer and select Add > New Item). From the Web category, select the Authentication Domain Service template. Name it TasksAuthenitcationDomainService. A class similar to the following will be added. The only modifications I made here are to remove some comments and change the name of the user class used by this service so it does not conflict in name with the one that is defined as part of our Entity Framework model.
[EnableClientAccess]
public class TasksAuthenticationDomainService : AuthenticationBase<AuthUser>
{
}
public class AuthUser : UserBase
{
}
The AuthenticationBase class takes care of calling into the membership provider when the user logs in. Because this class is a domain service, a client domain context will be generated and gets tied in automatically with the other domain contexts so that they can secure their calls based on this authentication service as well. The UserBase class provides the basic information about an authenticated user such as the identity name, roles and so on. You can derive from this to add any user specific properties that you want to associate with the authenticated user. The authentication service will return an instance of that user type to the client side after successful authentication so that it provides the full context of who the user is and what they can do, in addition to whatever other user specific information you want to add on.
Part of the client generated code includes a class called WebContext. This class gets enhanced after adding your authentication service to include a property called User of the AuthUser type used by the authentication service. The base class exposes methods for Login and other authentication and authorization related functionality. Even though this type gets defined through the code generation on the client side, you need to initialize the web context for the client and set its authentication type. To initialize the web context, you add an instance of it to the ApplicationLifetimeObjects collection of the App class in the App constructor after setting its Authentication property:
public App()
{
this.Startup += this.Application_Startup;
this.Exit += this.Application_Exit;
this.UnhandledException += this.Application_UnhandledException;
InitializeComponent();
WebContext context = new WebContext();
context.Authentication = new FormsAuthentication();
ApplicationLifetimeObjects.Add(context);
}
From there it is up to you to decide when and where to authenticate the user in the client application. But if you are going to secure the services, it needs to be before you start calling the domain services. In the TaskManager application, I went with the simplest, crudest approach. As the MainPage loads, it pops a ChildWindow derived pop up LoginForm that lets the user log in. If log in is successful, the tasks view is loaded. I factored out the XAML that was previously part of the MainPage markup into a separate user control called TasksView that I load into a ContentControl in the MainPage if log in is successful to defer the calls to the services until after login is successful. The code in the MainPage code behind now looks like this:
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
Loaded += OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
if (!WebContext.Current.Authentication.User.Identity.IsAuthenticated)
{
LoginForm login = new LoginForm();
login.Closed += (s, e2) =>
{
TasksView view = new TasksView();
MainContent.Content = view;
};
login.Show();
}
}
The LoginForm ChildWindow code looks like the following
public partial class LoginForm : ChildWindow
{
public LoginForm()
{
InitializeComponent();
}
private void OKButton_Click(object sender, RoutedEventArgs e)
{
LoginOperation loginOp = WebContext.Current.Authentication.Login(
new LoginParameters(UsernameTextBox.Text, PasswordTextBox.Text));
loginOp.Completed += (s2, e2) =>
{
if (loginOp.HasError)
{
errorTextBlock.Text = loginOp.Error.Message;
loginOp.MarkErrorAsHandled();
return;
}
else if (!loginOp.LoginSuccess)
{
errorTextBlock.Text = "Login failed.";
return;
}
else
{
errorTextBlock.Text = string.Empty;
DialogResult = true;
}
};
}
}
The LoginForm code calls the Login method on the WebContext.Authentication property that was initialized in the App class. The user can only get past the login dialog if they successfully authenticate. If their credentials are invalid, the Login call will succeed, but the LoginSuccess property on the LoginOperation result will be set to false.
With this in place, you now have everything you need for end-to-end authentication and authorization in the client and services.
All it takes to protect your domain services from unauthenticated users now is a single attribute: [RequiresAuthentication]. This attribute can be placed on the domain service class and all exposed operations on the class (Query, Insert, Update, Delete, and Invoke methods) will be protected. You can also place it at the method level if there are only particular methods you want to require authentication for. For example, in a product catalog scenario, you might allow anyone to query the catalog, but only specific users can modify the catalog.
For the TasksDomainService, I am securing all calls:
[EnableClientAccess()]
[RequiresAuthentication]
public class TasksDomainService : LinqToEntitiesDomainService<TaskManagerEntities>
{ }
A quick way to prove to yourself that the authentication is working is to override the Initialize method in your domain service and check the user identity on the context. You can also capture the authenticated user and use it a moment later when the domain service method is called.
private IPrincipal _User;
public override void Initialize(DomainServiceContext context)
{
base.Initialize(context);
Debug.WriteLine(context.User.Identity.Name);
_User = context.User;
}
Once you have verified who the user is, you may want to make decisions about what they can do, both server side and client side. To do so, you need to enable the role provider on the server side and have a configured role provider that will answer questions like “what roles is this user in” and “is this user in this role”. Then you can make role assertions on the server and client side and either prevent the user from executing some logic if they are not in the right role, filter data based on them not being in a particular role, or modify the UI based on their role.
Like the membership provider, you can use the default SQL Server provider, a Windows provider that is in the framework, or write your own custom provider. For the sample application, I wrote a simple custom provider. Like the membership provider, there is only one method you have to implement to support authorization at runtime, GetRolesForUser:
public class CustomRoleProvider : RoleProvider
{
public override string[] GetRolesForUser(string username)
{
if (username == "Brian") return new string[] { "Manager" };
else return new string[]{};
}
public override string ApplicationName
{
get { return "TaskManager"; }
set { }
}
// Other overrides not implemented
...
}
In your web.config file for the domain services host, ensure the role manager is enabled and your provider is specified if not the default:
<roleManager enabled="true" defaultProvider="myCustomProvider">
<providers>
<add name="myCustomProvider" type="TaskManager.Web.CustomRoleProvider,TaskManager.Web"/>
</providers>
</roleManager>
To ensure that only users in a certain role are allowed to execute a particular domain service method, you use the [RequiresRole] attribute. It takes a params string[] of role names. For example, if only managers are allowed to add new customers, I can enforce that in the domain service like so:
[RequiresRole("Manager")]
public void InsertCustomer(Customer customer)
{
}
If you then add a customer on the client side and you are not in the Manager role, when SubmitChanges is called on the domain context, you will get an access denied exception:
You might also need to filter what data is exposed based on who the user is or what role they are in. In the sample application, I have the requirement that users in the Manager role can see all Tasks, but other users can only see the tasks for which they are in the associated users collection (many-to-many relationship between Tasks and Users at the database).
To do this, I can use roles and user identity inside my domain service methods. The authenticated user was captured in the Initialize method described in the previous section. I can now modify the GetTasks method like so:
public IQueryable<Task> GetTasks()
{
if (_User.IsInRole("Manager"))
return this.ObjectContext.Tasks.Include("TimeEntries").
Include("Customer").OrderBy(t => t.StartDate);
else
{
return ObjectContext.Tasks.Include("TimeEntries").Include("Customer").
Where(t=> t.Users.Where(u=>u.Username == _User.Identity.Name).FirstOrDefault() != null).
OrderBy(t=>t.StartDate);
}
}
You can see that if the user’s role is Manager, all Tasks are retrieved as before (possibly filtered by the client side expression tree sent when the method is called), but if the user is not in the Manager role, the results are filtered server side to only return the Tasks for which they are linked as a user on that Task.
If the user is not a Manager, then the Add Customer button on the client side should never be enabled in the first place (or possibly hidden) so that the access denied exception is never reached. To do this, you need to check the user’s roles on the client side. This is easy to do through the WebContext since it’s User property will be populated after authenitcation is complete.
The view model previously had a CanExecute handler for the AddCustomerCommand that was mistakenly driving enablement off of the selection of a Task. The adding of a customer is actually decoupled from the task selection, but now I want the command to be disabled if the user is not a manager. So I simply updated the CanExecute handler for the AddCustomerCommand in the view model to the following:
private bool OnCanAddCustomer(object arg)
{
if (WebContext.Current.User.IsInRole("Manager"))
return true;
else
return false;
}
Since the authentication is complete before the view and view model load, the WebContext will already know who the user is and have the roles the user is associated with populated by the back end.
In the sample application, the data and credentials are all being passed in clear text when the messages go back and forth from the client to the server and back. Obviously that is not a good idea for security. When you are securing your site and using WCF RIA Services, you really need to use SSL to protect the messages. To enforce that the service does not get deployed without the protection of SSL, you can simple add a property to the [EnableClientAccess] attribute in your domain services:
[EnableClientAccess(RequiresSecureEndpoint=true)]
In this article I have walked you through the core security features of WCF RIA Services that allow you to easily authenticate the user and authorize what actions they can take in the client and in the services. There are a number of additional things you can do including performing more complex authentication inside your authentication service and defining custom authorization attributes.
If you want more explicit control over the authentication process inside of your services, you can override the ValidateUser method on the AuthenticationDomainService derived class you add to your server project. Then instead of doing what the base class does – call out to the configured membership provider – you are in complete control inside of your service to do whatever lookup of the client credentials you need. The advantage of the membership provider approach is that the provider you write becomes reusable not only in a WCF RIA Services application, but can also be used with ASP.NET, WCF, and even WPF and Windows Forms clients through Client Application Services. The advantage to bringing it inside your domain services is that you can more tightly integrate the user object used for authentication and a domain object like the separate User type in the Tasks Entity Framework model. For a good article and example of that, see the BookClub RIA Services sample and the authenitcation and authorization posts by Nikhil Kothari.
You can download the sample code for this article here.
About the Author
Brian Noyes is Chief Architect of IDesign, a Microsoft Regional Director, and Connected System MVP. He is a frequent top rated speaker at conferences worldwide including Microsoft TechEd, DevConnections, DevTeach, and others. He is the author of Developing Applications with Windows Workflow Foundation, Smart Client Deployment with ClickOnce, and Data Binding in Windows Forms 2.0. Brian got started programming as a hobby while flying F-14 Tomcats in the U.S. Navy, later turning his passion for code into his current career. You can contact Brian through his blog at http://briannoyes.net/ or on twitter @briannoyes.
原文地址:http://www.silverlightshow.net/items/WCF-RIA-Services-Part-7-Authentication-and-Authorization.aspx