Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Un Membership Provider con SQLite

Personalizzare il provider per la gestione degli accessi e realizzarlo con SQLite
Personalizzare il provider per la gestione degli accessi e realizzarlo con SQLite
Link copiato negli appunti

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.

Figura 1. Schema di un provider
Schema di un provider

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.
Email 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à).

Visualizza le classi

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.

Ti consigliamo anche