Il Membership Provider è il meccanismo messo a disposizione da ASP.NET 2.0 che permette di gestire l'autenticazione all'interno di una applicazione Webin modo completamente automatizzato, soprattutto se affiancato da controlli lato server come Login
e LoginView
.
In generale, quindi, non sarà più necessario scrivere manualmente tutta la logica di registrazione, login e di gestione degli utenti, compiti di cui si occuperà ASP.NET.
Un provider concettualmente non è niente di nuovo: è una classe che fa da tramite tra il database e l'applicazione Web.
Per chi sviluppa applicazioni di una certa complessità, che si interfacciano con basi di dati, questo approccio rimane sicuramente familiare: l'utilizzo di un provider permette, infatti, di raggiungere quel livello di astrazione necessario a far si che l'applicazione rimanga completamente indipendente dalla struttura fisica dei dati.
Lo sviluppatore, quindi, ha a che fare solamente con il provider e non deve necessariamente occuparsi dell'accesso ai dati, che dovrebbe poter essere indipendente e poter sfruttare un qualsiasi DBMS (o base di dati in genere). Tutto ciò di cui abbiamo bisogno per cambiare database è un nuovo provider scritto su misura per la fonte che vogliamo utilizzare (ad esempio: Sql Server, XML, file di testo, Web Service, solo per citarne alcune molto diverse tra loro).
La novità principale risiede nell'utilizzo che possiamo fare dei provider: utilizzare i servizi predefiniti di ASP.NET (Membership, Role, etc.) con database diversi da quello supportato in modo nativo (Sql Server), sfruttando quindi la potenza e la flessibilità degli oggetti che già esistono all'interno del Framework.
Una particolarità dei provider riguarda il loro ciclo di vita: quando l'applicazione Web viene avviata, ad esempio quando viene richiesta una pagina per la prima volta, ASP.NET cerca nel file Web.config quali sono i provider che devono essere caricati.
Il caricamento di un provider avviene tramite l'utilizzo di una classe statica, che funge anche da proxy fra la nostra applicazione ed il provider vero e proprio.
Una classe statica si è rivelata la scelta migliore per svolgere questo compito, in quanto viene creata all'avvio dell'applicazione e distrutta, escludendo situazioni particolari, quando questa viene fermata o riavviata. Per esempio la classe statica che si occupa di caricare i Membership Provider è System.Web.Security.Membership
.
Pensando a una organizzazione dell'applicazione sui livelli di "Data Access Layer" e "Business Logic Layer", potremmo in modo approssimativo affermare che il provider corrisponde al primo e la classe statica al secondo.
In questo articolo vedremo come realizzare un Membership Provider personalizzato per SQLite, particolarmente utile per siti di piccola e media dimensione o in scenari in cui non è possibile utilizzare altri database.
Il database
Cominciamo la realizzazione del nostro provider creando il database, che conterrà una sola tabella, aspnet_Users, con i seguenti campi:
Nome | Tipo | Descrizione |
---|---|---|
PKID | TEXT | Guid dell'utente. Primary Key. |
Username | TEXT | Nome utente. |
ApplicationName | TEXT | Nome dell'applicazione a cui l'utente appartiene. E' possibile, infatti, utilizzare lo stesso database per più progetti all'interno dello stesso sito internet, a patto che questi abbiamo nomi diversi. |
TEXT | Email dell'utente. | |
Comment | TEXT | Commento sull'utente. |
Password | TEXT | La password. |
PasswordQuestion | TEXT | La domanda per recuperare la password in caso di perdita della stessa. |
PasswordAnswer | TEXT | La risposta alla domanda. |
IsApproved | INTEGER | Indica se l'utente è stato approvato, magari attraverso una procedura personalizzata di verificata (ad esempio tramite email). |
LastActivityData | TEXT | Data dell'ultima attività dell'utente. |
LastLoginDate | TEXT | Data dell'ultimo login. |
LastPasswordChangedDate | TEXT | Data dell'ultimo cambiamento della password. |
CreationDate | TEXT | Data di creazione dell'account. |
IsOnLine | INTEGER | Indica se l'utente è online. |
IsLockedOut | INTEGER | Indica se all'utente è negato l'accesso, magari dopo un certo numero di login errati. |
LastLockedOutDate | TEXT | Data dell'ultima volta in cui l'accesso è stato vietato. |
FailedPasswordAttemptCount | INTEGER | Numero di fallimenti per il recupero della password. |
FailedPasswordAttemptWindowStart | TEXT | Data del primo fallimento dell'operazione di recupero della password. Utile per limitare il numero di recuperi all'interno di un intervallo di tempo. |
FailedPasswordAnswerAttemptCount | INTEGER | Numero di fallimenti nell'inserimento della risposta segreta per il recupero della password |
FailedPasswordAnswerAttemptWindowStart | TEXT | Data del primo fallimento di recupero della password tramite risposta segreta. Utile per limitare il numero di recuperi all'interno di un intervallo di tempo. |
Abbiamo utilizzato i tipi di dati predefiniti di SQLite, senza ricorrere alla Column Affinity, per cui i campi che contengono date, ad esempio LastLoginDate
, sono dichiarati come TEXT
e campi che contengono valori boolean
, come IsOnLine
, sono dichiarati come INTEGER
.
La tabella può essere creata manualmente all'interno di un programma come SQLite Administrator
, oppure attraverso SQL.
Creazione della tabella degli utenti
CREATE TABLE aspnet_Users (
PKID TEXT PRIMARY KEY NOT NULL,
Username TEXT NOT NULL,
ApplicationName TEXT NOT NULL,
Email TEXT NOT NULL,
Comment TEXT default NULL,
Password TEXT NOT NULL,
PasswordQuestion TEXT default NULL,
PasswordAnswer TEXT default NULL,
IsApproved INTEGER default NULL,
LastActivityDate TEXT default NULL,
LastLoginDate TEXT default NULL,
LastPasswordChangedDate TEXT default NULL,
CreationDate TEXT default NULL,
IsOnLine INTEGER default NULL,
IsLockedOut INTEGER default NULL,
LastLockedOutDate TEXT default NULL,
FailedPasswordAttemptCount INTEGER default NULL,
FailedPasswordAttemptWindowStart TEXT default NULL,
FailedPasswordAnswerAttemptCount INTEGER default NULL,
FailedPasswordAnswerAttemptWindowStart TEXT default NULL
);
Un database già pronto all'uso si trova nel file allegato all'articolo.
Il file Web.config
Prima di passare al codice vero e proprio, vediamo come modificare il Web.config. Vogliamo utilizzare, modificandolo, lo stesso meccanismo di Membership già presente nel .Net Framework: inseriamo, quindi, nella sezione <system.Web>
le solite righe per configurare l'autenticazione.
Web config
<membership defaultProvider="SQLiteMembershipProvider" userIsOnlineTimeWindow="30">
<providers>
<clear/>
<add
name="SQLiteMembershipProvider"
type="Example.Providers.SQLiteMembershipProvider"
connectionStringName="SQLiteConnString"
applicationName="Example"
enablePasswordRetrieval="false"
enablePasswordReset="true"
requiresQuestionAndAnswer="true"
requiresUniqueEmail="true"
passwordFormat="Hashed"
passwordAttemptWindow="10"
maxInvalidPasswordAttempts="5"
minRequiredPasswordLength="7"
minRequiredNonalphanumericCharacters="1"
/>
</providers>
</membership>
In questo modo eliminiamo qualsiasi provider già configurato attraverso l'istruzione <clear />
, per poi aggiungere il nostro provider personalizzato Example.Providers.SQLiteMembershipProvider
. Le impostazioni di quest'ultimo sono quelle predefinite del Membership Provider di ASP.NET; per maggiori informazioni si può consultare la documentazione ufficiale di MSDN.
Possiamo ora configurare la stringa di connessione.
La stringa di connessione
<connectionStrings>
<remove name="SQLiteConnString" />
<add
connectionString="Data Source=|DataDirectory|Example.db;"
name="SQLiteConnString" providerName="System.Data.SQLite"
/>
</connectionStrings>
Nell'esempio il database si chiama Example.db
e si trova nella cartella App_Data
del progetto.
Il provider
Apriamo Visual Studio o Visual Web Delevoper Express e, dopo aver creato un nuovo progetto, aggiungiamo una classe chiamata SQLiteMembershipProvider
, che eredita dalla classe astratta System.Web.Security.MembershipProvider
, la quale estende a sua volta la System.Configuration.Provider.ProviderBase
(non mostrata in figura per semplicità).
L'immagine mostra i metodi e le proprietà ereditati che dovranno essere sovrascritti, più alcune funzioni dichiarate come private, che svolgono compiti di supporto.
A causa del numero abbastanza elevato di metodi sarà impossibile analizzarli tutti in modo dettagliato e ci concentremo, quindi, su due tra i più significativi. Questo non dovrebbe comunque rappresentare un problema, in quanto la logica di funzionamento è abbastanza simile anche per tutte le procedure che non verranno prese in esame. Inoltre, il codice completo è comunque disponibile nel file allegato all'articolo, che invito a scaricare per riferimento.
La parte fondamentale nella scrittura di un provider consiste nella funzione Initialize, ereditata da ProviderBase: essa viene infatti richiamata dalla classe statica per inizializzare il provider secondo le impostazioni contenute nel file Web.config
.
Inizializzazione del provider
public override void Initialize(string name, NameValueCollection config)
{
if (config == null)
throw new ArgumentNullException("config");
// Carico il nome del provider
if (string.IsNullOrEmpty(name))
name = "SQLiteMembershipProvider";
// Carico la descrizione
if (string.IsNullOrEmpty(config["description"]))
{
config.Remove("description");
config.Add("description", "SQLite Membership provider.");
}
// Chiamo il metodo di base
base.Initialize(name, config);
// Carico i valori di configurazione
applicationName = GetConfigValue(config["applicationName"], System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath);
maxInvalidPasswordAttempts = Convert.ToInt32(GetConfigValue(config["maxInvalidPasswordAttempts"], "5"));
passwordAttemptWindow = Convert.ToInt32(GetConfigValue(config["passwordAttemptWindow"], "10"));
minRequiredNonAlphanumericCharacters = Convert.ToInt32(GetConfigValue(config["minRequiredNonAlphanumericCharacters"], "1"));
minRequiredPasswordLength = Convert.ToInt32(GetConfigValue(config["minRequiredPasswordLength"], "7"));
passwordStrengthRegularExpression = Convert.ToString(GetConfigValue(config["passwordStrengthRegularExpression"], ""));
enablePasswordReset = Convert.ToBoolean(GetConfigValue(config["enablePasswordReset"], "true"));
enablePasswordRetrieval = Convert.ToBoolean(GetConfigValue(config["enablePasswordRetrieval"], "true"));
requiresQuestionAndAnswer = Convert.ToBoolean(GetConfigValue(config["requiresQuestionAndAnswer"], "false"));
requiresUniqueEmail = Convert.ToBoolean(GetConfigValue(config["requiresUniqueEmail"], "true"));
string temp_format = GetConfigValue(config["passwordFormat"], "Hashed");
switch (temp_format)
{
case "Hashed":
passwordFormat = MembershipPasswordFormat.Hashed;
break;
case "Encrypted":
passwordFormat = MembershipPasswordFormat.Encrypted;
break;
case "Clear":
passwordFormat = MembershipPasswordFormat.Clear;
break;
default:
throw new ProviderException("Password format not supported.");
}
// Connection string
ConnectionStringSettings ConnectionStringSettings = ConfigurationManager.ConnectionStrings[config["connectionStringName"]];
if (ConnectionStringSettings == null || string.IsNullOrEmpty(ConnectionStringSettings.ConnectionString))
throw new ProviderException("Connection string cannot be blank.");
connectionString = ConnectionStringSettings.ConnectionString;
}
Dopo essersi assicurato che esista la sezione di configurazione necessaria al provider, il programma continua inizializzando tutte le opzioni necessarie.
Il metodo GetConfigValue
, dichiarato come privato, controlla che l'impostazione specificata esista nel Web.config, caricandone in questo caso il valore o in caso contrario uno di default.
Alla fine viene caricata anche la stringa di connessione e si solleva un'eccezione nel caso che questa non venga trovata.
La seconda funzione che analizzeremo è invece una di quelle ereditate da MembershipProvider
, in particolare GetUserNameByEmail
, che si occupa di trovare l'utente a cui appartiene un dato indirizzo email. Essendo una delle più semplici, mi è sembrata la scelta migliore.
Override di GetUserNameByEmail()
public override string GetUserNameByEmail(string email)
{
SQLiteConnection conn = new SQLiteConnection(connectionString);
SQLiteCommand cmd = new SQLiteCommand("SELECT Username" +
" FROM [" + tableName + "] WHERE Email = ? AND
ApplicationName = ?", conn);
cmd.Parameters.Add("@Email", DbType.String, 128).Value = email;
cmd.Parameters.Add("@ApplicationName", DbType.String, 255).Value = applicationName;
string username = "";
try
{
conn.Open();
username = (string)cmd.ExecuteScalar();
}
catch (SQLiteException e)
{
throw e;
}
finally
{
conn.Close();
}
if (username == null) username = "";
return username;
}
Per prima cosa viene creata la connessione al database SQLite, seguita dall'oggetto SQLiteCommand
per eseguire la query; esecuzione che avviene all'interno di un blocco try.
Notare che nella nostra implementazione le possibili eccezioni generate vengono per semplicità reindirizzate verso la classe che ha invocato il metodo.
Conclusioni
Dopo aver fornito una breve panoramica sul funzionamento dei provider di ASP.NET, è stata introdotta la creazione di un Membership Provider personalizzato per SQLite, esaminando la struttura del database ed alcune funzioni.
Come ulteriore riferimento per la scrittura di provider personalizzati vi rimando all'ottima sezione di MSDN, Provider Toolkit.