Introduzione
Uno dei problemi più comuni che si devono affrontare nello sviluppo di progetti web riguarda la gestione della sicurezza nelle applicazioni web e quindi la personalizzazione dei provider di ASP.NET relativi a
Membership, Roles e Profile.
ASP.NET 2.0 ci fornisce infatti alcuni provider già pronti (SqlMembershipProvider e ActiveDirectoryMembershipProvider) ma il più delle volte si ha l’esigenza di utilizzare una base dati già esistente o da definire, e quindi ci si trova di fronte al problema di dover implementare dei provider specifici.
A chi non ha abbastanza familiarità con il provider model di ASP.NET 2.0 e di come venga sfruttato dai vari controlli web suggerisco la lettura di questi due articoli:
Aree di accesso protette con ASP.NET 2.0: Membership e Roles API La classe Profile di ASP.NET 2.0 Scenario
Come caso di esempio si supponga di dover creare un sito web con delle aree a cui possono accedere solo gli utenti registrati. Questi utenti saranno definiti con i seguenti ruoli di accesso:
? User
? Power User
? AdministratorCome detto in precedenza non si vuole utilizzare il provider predefinito di ASP.NET, e quindi il relativo database, ma si ha l’esigenza che l'elenco degli utenti e la loro tipologia venga caricata da un database Sql Server che ha la seguente struttura:
La tabella
Users contiene l'elenco degli utenti e lo
UserTypeID rappresenta la tipologia di utente. Ogni utente che si autentica sul sito potrà accedere a determinate pagine in base al ruolo associato (
UserType).
La struttura del sito web (definito come Web Application Project) è la seguente:
Ci sono due cartelle
Admin e
User che conterranno le pagine a cui possono accedere solo gli amministratori e gli utenti registrati. Le pagine Default e Login invece sono visibili a tutti gli utenti, compresi gli anonimi.
Solution
La solution che andremo a creare conterrà tre progetti:
- Sito web: MyWebSite
- Class library per l’accesso ai dati: MyWebSite.Data
- Class library con la business logic: MyWebSite.BusinessAll'interno del progetto
MyWebSite.Business verrà definita la classe
User che rappresenta un utente e l'enum con le tipologie di utenti:
Anche la class library
MyWebSite.Data sarà abbastanza semplice, conterrà le classi che si occuperanno dell'accesso a
SQL Server:
MembershipProvider
Adesso che è stata definita la struttura si può procedere con l’implementazione del
MembershipProvider, per avere una corretta validazione degli utenti che effettuano il login sul sito. Basterà creare una classe che eredita da
System.Web.Security.MembershipProvider ed effettuare l'override delle funzioni che ci interessano. In questo caso specifico verrà implementata solo la funzione
ValidateUser che si occupa di verificare i dati di accesso di un utente:
namespace MyWebSite.Providers
{
public class MyMembershipProvider : System.Web.Security.MembershipProvider
{
public override bool ValidateUser(string username, string password)
{
//Richiama la stored di validazione dell'utente
return Business.User.ValidateLogin(username, password);
}
#region "NonImplementate"
...
#endregion
}
}
Come si può vedere dal codice la funzione non fa altro che richiamare il metodo
ValidateLogin della classe
Business.User, la quale a sua volta, attraverso la classe
Data.User richiamerà una stored procedure per la validazione dei dati dell’utente:
CREATE PROCEDURE [Common].[proc_Users_ValidateLogin]
@Username varchar(20),
@Password varchar(15)
AS
BEGIN
SET NOCOUNT ON
SELECT
userID
FROM
Common.Users
WHERE
Username = @Username
AND [Password] = @Password
END
Di tutte le altre funzioni bisogna comunque fare l'override perchè la classe
MembershipProvider è una classe astratta:
public override MembershipUser CreateUser(string username, string password,
string email, string passwordQuestion, string passwordAnswer, bool isApproved, bject providerUserKey, out MembershipCreateStatus status)
{
//Metodo non implementato
throw new NotImplementedException();
}
Se ci sarà l’esigenza di utilizzare gli altri controlli web che
ASP.NET fornisce (
CreateUserWizard, ChangePassword ecc.) basterà implementare anche i metodi del provider che questi controlli andranno a richiamare, per evitare che vengano generate delle eccezioni:
Una volta creato il provider si potrà procedere con la configurazione del tag
all’interno del
web.config della nostra applicazione affinché lo utilizzi correttamente:
<system.web>
...
<!-- Autenticazione Forms -->
<authentication mode="Forms">
<forms loginUrl="~/Login.aspx" defaultUrl="~/Default.aspx"/>
</authentication>
<!-- Acceso a tutti gli utenti -->
<authorization>
<allow users="*"/>
</authorization>
<!--
La classe MyMembershiProvider viene aggiunta all'elenco dei provider
e impostata come defaultProvider
-->
<membership defaultProvider="MyMembershipProvider">
<providers>
<clear/>
<add name="MyMembershipProvider" type="MyWebSite.Providers.MyMembershipProvider"/>
</providers>
</membership>
...
</system.web>
A questo punto basterà inserire un controllo
Login in una pagina web e provare ad effettuare un accesso.
RoleProvider
Il passo successivo consiste nel gestire i ruoli associati all'utente. Nel nostro caso abbiamo detto che il ruolo sarà definito dal campo
UserTypeID e dalla relativa enum quindi i ruoli disponibili saranno:
• User
• PowerUser
• AdministratorAll'interno del sito web sono state definite due cartelle (
Admin e User)
con le seguenti impostazioni di accesso (inserite nel web.config della cartella):
• Admin: solo utenti che hanno effettuato il login e con ruolo administrator
<configuration>
<system.web>
<authorization>
<allow roles="Administrator"/>
<deny users="*"/>
</authorization>
</system.web>
</configuration>
• User: tutti gli utenti non anonimi
<configuration>
<system.web>
<authorization>
<deny users="?"/>
</authorization>
</system.web>
</configuration>
Il passo successivo consiste nel creare una classe
MyRoleProvider che eredita da
System.Web.Security.RoleProvider ed implementa il metodo
GetRolesForUser (elenco dei ruoli di un utente). Nel nostro caso viene caricata la classe
User e, se l'utente esiste, viene ritornata la stringa che rappresenta il valore dell'enum.
namespace MyWebSite.Providers
{
public class MyRoleProvider : System.Web.Security.RoleProvider
{
public override string[] GetRolesForUser(string username)
{
//Carica i dati utente
Business.User user = Business.User.GetUser(username);
if (user != null)
{
//ritorna il tipo utente come nome del ruolo
return new string[] { user.Type.ToString() };
}
return new string[] { };
}
...
}
}
All’interno del
web.config andrà abilitata e configurata la sezione
roleManager:
<roleManager enabled="true" defaultProvider="MyRoleProvider">
<providers>
<clear/>
<add name="MyRoleProvider" type="MyWebSite.Providers.MyRoleProvider"/>
</providers>
</roleManager>
ProfileProvider
L'ultimo Provider che risulta molto comodo implementare è il
ProfileProvider. Questo provider ci consente di accedere a delle proprietà di contorno dell'utente che vengono definite nel
web.config:
<profile defaultProvider="MyProfileProvider" enabled="true">
<providers>
<add name="MyProfileProvider" type="MyWebSite.Providers.MyProfileProvider"/>
</providers>
<properties>
<add name="Name" type="String"/>
<add name="Surname" type="String"/>
<add name="Type" type="MyWebSite.Business.UserType"/>
<add name="Email" type="String"/>
</properties>
</profile>
Come si può vedere sono state esposte le seguenti proprietà:
- Name
- Surname
- Type
- EmailLa classe che definisce il provider personalizzato è la seguente:
namespace MyWebSite.Providers
{
public class MyProfileProvider : System.Web.Profile.ProfileProvider
{
public override SettingsPropertyValueCollection GetPropertyValues(SettingsContext context, SettingsPropertyCollection collection)
{
//Crea la collection che conterrà i valori
SettingsPropertyValueCollection settings = new SettingsPropertyValueCollection();
if (collection.Count == 0)
return settings;
//Popola la collection
foreach (SettingsProperty prop in collection)
{
settings.Add(new SettingsPropertyValue(prop));
}
//Preleva Username dal context
string username = Convert.ToString(context["UserName"]);
if (!string.IsNullOrEmpty(username))
{
//Carica istanza dell'oggetto utente
Business.User objUser = Business.User.GetUser(username);
if (objUser != null)
{
//Riferimento al tipo di classe dell'oggetto user
Type objType = objUser.GetType();
//Cicla l'elenco delle proprietà da valorizzare
foreach (SettingsPropertyValue prop in settings)
{
//Ricerca la property
System.Reflection.PropertyInfo propInfo = objType.GetProperty(prop.Name);
//Verifica esistenza della property
if (propInfo == null)
throw new ProviderException(String.Format("Impossibile trovare la proprietà '{0}' specificata nel profilo", prop.Name));
//Preleva il valore della property dall'istanza della classe
prop.PropertyValue = propInfo.GetValue(objUser, null);
}
}
else
throw new ProviderException(String.Format("Impossibile caricare i dati dell'utente '{0}'", username));
}
return settings;
}
public override void SetPropertyValues(SettingsContext context, SettingsPropertyValueCollection collection)
{
//La fase di salvataggio delle proprietà non è implementata
//throw new NotImplementedException();
}
...
}
}
E' stato fatto l’override di due metodi:
• GetPropertyValues: carica i valori delle proprietà associate al profilo
• SetPropertyValues: salva i valori delle proprietà associate al profilo
La fase di lettura dei dati è stata implementata sfruttando la
Reflection che ci consente di prelevare dinamicamente i valori delle property della classe
Business.User.
Il salvataggio invece non è stato implementato perché per quest’applicazione di esempio non è prevista la possibilità di salvare dei dati del profilo. Nulla vieta di farlo sfruttando sempre le potenzialità della
Reflection.
Per chi ha familiarità con i
WebSite noterà che nei
WebProject la classe
Profile non viene generata automaticamente in base alle proprietà del profilo definite nel web.config.
Per risolvere questo problema ci sono due possibilità:
• Definire una classe profile che eredita da System.Web.Profile.ProfileBase e si occupa del caricamento dati
• Utilizzare il tool gratuito
Web Profile Generator Io personalmente utilizzo sempre il tool perché risulta molto comodo. Se avete
Visual Studio 2005 lo potete installare attraverso il setup e successivamente troverete una voce
Generate Web Profile nel menu contestuale del
web.config:
Questa opzione genererà in automatico una classe
WebProfile che espone tutte le proprietà definite nel
web.config. Per poter accedere al profilo corrente basterà utilizzare la proprietà statica
Current della classe
WebProfile:
if (!WebProfile.Current.IsAnonymous)
{
string strSurname = WebProfile.Current.Surname;
...
}
Per chi possiede
Visual Studio 2008 suggerisco la lettura questo articolo per poter utilizzare l'
Add-In anche nel nuovo ambiente di sviluppo:
Load VS2005 Add-in In VS2008 Un'ultima ottimizzazione che possiamo fare è quella di caricare i dati dell'utente in
Session, per evitare di accedere al database tutte le volte che viene richiamato il provider dei ruoli e quello del profilo. Questa opzione è configurabile a livello di
web.config attraverso il parametro
UserDataInSession:
<appSettings>
<add key="UserDataInSession" value="1"/>
</appSettings>
All'interno del progetto è stata definita una nuova classe
UserLoader che si occupa della fase di caricamento dati dell’utente (metodo
GetUserObject) con relativa gestione dei dati in
Session:
namespace MyWebSite
{
public class UserLoader
{
private const string SessionKey = "User";
/// <summary>
/// Ritorna la classe User dell'utente loggato
/// </summary>
public static Business.User GetUserObject(string username)
{
HttpContext context = HttpContext.Current;
//Abilitato caricamento dati dalla session
if (Config.UserDataInSession && context.Session != null)
{
if (context.Session[SessionKey] == null)
{
//Aggiunge i dati in session
context.Session.Add(SessionKey, Business.User.GetUser(username));
}
return context.Session[SessionKey] as Business.User;
}
else
{
//Carica i dati da db
return Business.User.GetUser(username);
}
}
/// <summary>
/// Cancella i dati dell'utente dalla memoria
/// </summary>
public static void ClearCache()
{
HttpContext context = HttpContext.Current;
if (context.Session != null && context.Session[SessionKey] != null)
{
context.Session[SessionKey] = null;
}
}
}
}
All'interno dei provider basterà sostituire la fase di caricamento dei dati dell'utente:
//Vecchia versione
Business.User objUser = Business.User.GetUser(username);
//Nuova versione
Business.User objUser = UserLoader.GetUserObject(username);
Conclusione
A questo punto, una volta definiti tutti e tre i provider possiamo procedere con lo sviluppo della nostra applicazione sfruttando tutte le potenzialità che
ASP.NET ci fornisce, certi che avremo una struttura affidabile e allo stesso tempo facilmente configurabile. Se in un futuro ci sarà l'esigenza di modificare il tipo di provider sarà sufficiente modificare la configurazione e tutto il resto continuerà a funzionare regolarmente. Allego il progetto di esempio realizzato con
Visual Studio .NET 2008.