Condividi tramite


Panoramica dei provider di archiviazione personalizzati per ASP.NET Identity

di Tom FitzMacken

ASP.NET Identity è un sistema estendibile che consente di creare un provider di archiviazione personalizzato e collegarlo all'applicazione senza lavorare nuovamente l'applicazione. Questo argomento descrive come creare un provider di archiviazione personalizzato per ASP.NET Identity. Illustra i concetti importanti per la creazione del provider di archiviazione, ma non è una procedura dettagliata per implementare un provider di archiviazione personalizzato.

Per un esempio di implementazione di un provider di archiviazione personalizzato, vedere Implementazione di un provider di archiviazione di identità personalizzato di MySQL ASP.NET.

Questo argomento è stato aggiornato per ASP.NET Identity 2.0.

Versioni software usate nell'esercitazione

  • Visual Studio 2013 con l'aggiornamento 2
  • ASP.NET identità 2

Introduzione

Per impostazione predefinita, il sistema di ASP.NET Identity archivia le informazioni utente in un database SQL Server e usa Entity Framework Code First per creare il database. Per molte applicazioni, questo approccio funziona bene. Tuttavia, è consigliabile usare un tipo diverso di meccanismo di persistenza, ad esempio Archiviazione tabelle di Azure, oppure è possibile che siano già presenti tabelle di database con una struttura molto diversa rispetto all'implementazione predefinita. In entrambi i casi, è possibile scrivere un provider personalizzato per il meccanismo di archiviazione e collegare tale provider all'applicazione.

ASP.NET Identity è incluso per impostazione predefinita in molti modelli di Visual Studio 2013. È possibile ottenere aggiornamenti per ASP.NET identity tramite il pacchetto NuGet Microsoft AspNet Identity EntityFramework.

Questo argomento include le sezioni seguenti:

Comprendere l'architettura

ASP.NET Identità è costituita da classi denominate manager e archivi. I manager sono classi di alto livello che uno sviluppatore di applicazioni usa per eseguire operazioni, ad esempio la creazione di un utente, nel sistema di ASP.NET Identity. Gli archivi sono classi di livello inferiore che specificano come le entità, ad esempio utenti e ruoli, vengono mantenute. Gli archivi sono strettamente associati al meccanismo di persistenza, ma i gestori vengono sconvolti dagli archivi, il che significa che è possibile sostituire il meccanismo di persistenza senza interrompere l'intera applicazione.

Il diagramma seguente illustra come l'applicazione Web interagisce con i responsabili e archivia l'interazione con il livello di accesso ai dati.

Diagramma che mostra come l'applicazione Web interagisce con i manager

Per creare un provider di archiviazione personalizzato per ASP.NET Identity, è necessario creare l'origine dati, il livello di accesso ai dati e le classi di archiviazione che interagiscono con questo livello di accesso ai dati. È possibile continuare a usare le stesse API di gestione per eseguire operazioni di dati sull'utente, ma ora che i dati vengono salvati in un sistema di archiviazione diverso.

Non è necessario personalizzare le classi di gestione perché quando si crea una nuova istanza di UserManager o RoleManager si specifica il tipo della classe utente e si passa un'istanza della classe store come argomento. Questo approccio consente di collegare le classi personalizzate alla struttura esistente. Verrà illustrato come creare un'istanza di UserManager e RoleManager con le classi di archivio personalizzate nella sezione Riconfigurare l'applicazione per usare il nuovo provider di archiviazione.

Comprendere i dati archiviati

Per implementare un provider di archiviazione personalizzato, è necessario comprendere i tipi di dati usati con identità ASP.NET e decidere quali funzionalità sono rilevanti per l'applicazione.

Dati Descrizione
Utenti Utenti registrati del sito Web. Include l'ID utente e il nome utente. Potrebbe includere una password hash se gli utenti accedono con le credenziali specifiche del sito (anziché usare le credenziali da un sito esterno come Facebook) e il timbro di sicurezza per indicare se qualsiasi elemento è stato modificato nelle credenziali utente. Potrebbe anche includere indirizzo di posta elettronica, numero di telefono, se è abilitata l'autenticazione a due fattori, il numero corrente di account di accesso non riuscito e se un account è stato bloccato.
Attestazioni utente Set di istruzioni (o attestazioni) sull'utente che rappresentano l'identità dell'utente. Può abilitare un'espressione maggiore dell'identità dell'utente che può essere ottenuta tramite ruoli.
Account di accesso utente Informazioni sul provider di autenticazione esterno (ad esempio Facebook) da usare durante l'accesso a un utente.
Ruoli Gruppi di autorizzazione per il sito. Include l'ID ruolo e il nome del ruolo , ad esempio "Amministrazione" o "Employee".

Creare il livello di accesso ai dati

In questo argomento si presuppone che si abbia familiarità con il meccanismo di persistenza che si intende usare e come creare entità per tale meccanismo. Questo argomento non fornisce informazioni dettagliate su come creare i repository o le classi di accesso ai dati; fornisce invece alcuni suggerimenti sulle decisioni di progettazione da prendere quando si lavora con ASP.NET Identity.

Si ha molta libertà quando si progettano i repository per un provider di archiviazione personalizzato. È necessario creare repository solo per le funzionalità che si intende usare nell'applicazione. Ad esempio, se non si usano ruoli nell'applicazione, non è necessario creare l'archiviazione per ruoli o ruoli utente. La tecnologia e l'infrastruttura esistente possono richiedere una struttura molto diversa dall'implementazione predefinita di ASP.NET Identity. Nel livello di accesso ai dati si fornisce la logica per lavorare con la struttura dei repository.

Per un'implementazione di MySQL dei repository dati per ASP.NET Identity 2.0, vedere MySQLIdentity.sql.

Nel livello di accesso ai dati viene fornita la logica per salvare i dati da ASP.NET Identità all'origine dati. Il livello di accesso ai dati per il provider di archiviazione personalizzato potrebbe includere le classi seguenti per archiviare le informazioni sull'utente e sul ruolo.

Classe Descrizione Esempio
Contesto Incapsula le informazioni da connettere al meccanismo di persistenza ed eseguire query. Questa classe è fondamentale per il livello di accesso ai dati. Le altre classi di dati richiedono un'istanza di questa classe per eseguire le operazioni. Si inizializzeranno anche le classi dell'archivio con un'istanza di questa classe. MySQLDatabase
Archiviazione utente Archivia e recupera le informazioni utente, ad esempio il nome utente e l'hash delle password. UserTable (MySQL)
Archiviazione ruoli Archivia e recupera informazioni sul ruolo, ad esempio il nome del ruolo. RoleTable (MySQL)
Archiviazione UserClaims Archivia e recupera le informazioni sull'attestazione utente, ad esempio il tipo di attestazione e il valore. UserClaimsTable (MySQL)
Archiviazione UserLogins Archivia e recupera le informazioni di accesso utente, ad esempio un provider di autenticazione esterno. UserLoginsTable (MySQL)
Archiviazione UserRole Archivia e recupera i ruoli assegnati a un utente. UserRoleTable (MySQL)

Di nuovo, è necessario implementare solo le classi che si intende usare nell'applicazione.

Nelle classi di accesso ai dati specificare il codice per eseguire operazioni di dati per il meccanismo di persistenza specifico. Ad esempio, all'interno dell'implementazione di MySQL, la classe UserTable contiene un metodo per inserire un nuovo record nella tabella di database Users. La variabile denominata _database è un'istanza della classe MySQLDatabase.

public int Insert(TUser user)
{
    string commandText = @"Insert into Users (UserName, Id, PasswordHash, SecurityStamp,Email,EmailConfirmed,PhoneNumber,PhoneNumberConfirmed, AccessFailedCount,LockoutEnabled,LockoutEndDateUtc,TwoFactorEnabled)
        values (@name, @id, @pwdHash, @SecStamp,@email,@emailconfirmed,@phonenumber,@phonenumberconfirmed,@accesscount,@lockoutenabled,@lockoutenddate,@twofactorenabled)";
    Dictionary<string, object> parameters = new Dictionary<string, object>();
    parameters.Add("@name", user.UserName);
    parameters.Add("@id", user.Id);
    parameters.Add("@pwdHash", user.PasswordHash);
    parameters.Add("@SecStamp", user.SecurityStamp);
    parameters.Add("@email", user.Email);
    parameters.Add("@emailconfirmed", user.EmailConfirmed);
    parameters.Add("@phonenumber", user.PhoneNumber);
    parameters.Add("@phonenumberconfirmed", user.PhoneNumberConfirmed);
    parameters.Add("@accesscount", user.AccessFailedCount);
    parameters.Add("@lockoutenabled", user.LockoutEnabled);
    parameters.Add("@lockoutenddate", user.LockoutEndDateUtc);
    parameters.Add("@twofactorenabled", user.TwoFactorEnabled);

    return _database.Execute(commandText, parameters);
}

Dopo aver creato le classi di accesso ai dati, è necessario creare classi di archiviazione che chiamano i metodi specifici nel livello di accesso ai dati.

Personalizzare la classe utente

Quando si implementa un provider di archiviazione personalizzato, è necessario creare una classe utente equivalente alla classe IdentityUser nello spazio dei nomi Microsoft.ASP.NET.Identity.EntityFramework :

Il diagramma seguente illustra la classe IdentityUser che è necessario creare e l'interfaccia da implementare in questa classe.

Immagine della classe Identity User

L'interfaccia IUser<TKey> definisce le proprietà che UserManager tenta di chiamare durante l'esecuzione di operazioni richieste. L'interfaccia contiene due proprietà: ID e UserName. L'interfaccia IUser<TKey> consente di specificare il tipo di chiave per l'utente tramite il parametro TKey generico. Il tipo della proprietà Id corrisponde al valore del parametro TKey.

Il framework Identity fornisce anche l'interfaccia IUser (senza il parametro generico) quando si vuole usare un valore stringa per la chiave.

La classe IdentityUser implementa IUser e contiene eventuali proprietà o costruttori aggiuntivi per gli utenti nel sito Web. Nell'esempio seguente viene illustrata una classe IdentityUser che usa un numero intero per la chiave. Il campo ID è impostato su int in modo che corrisponda al valore del parametro generico.

public class IdentityUser : IUser<int>
{
    public IdentityUser() { ... }
    public IdentityUser(string userName) { ... }
    public int Id { get; set; }
    public string UserName { get; set; }
    // can also define optional properties such as:
    //    PasswordHash
    //    SecurityStamp
    //    Claims
    //    Logins
    //    Roles
}

Per un'implementazione completa, vedere IdentityUser (MySQL).

Personalizzare l'archivio utenti

Si crea anche una classe UserStore che fornisce i metodi per tutte le operazioni sui dati sull'utente. Questa classe equivale alla classe UserStore<TUser> nello spazio dei nomi Microsoft.ASP.NET.Identity.EntityFramework . Nella classe UserStore implementi IUserStore<TUser, TKey> e tutte le interfacce facoltative. È possibile selezionare le interfacce facoltative da implementare in base alle funzionalità che si desidera fornire nell'applicazione.

L'immagine seguente mostra la classe UserStore che è necessario creare e le interfacce pertinenti.

Immagine della classe User Store

Il modello di progetto predefinito in Visual Studio contiene codice che presuppone che molte delle interfacce facoltative siano state implementate nell'archivio utenti. Se si usa il modello predefinito con un archivio utenti personalizzato, è necessario implementare interfacce facoltative nell'archivio utenti o modificare il codice del modello per non chiamare più i metodi nelle interfacce non implementate.

Nell'esempio seguente viene illustrata una classe di archivio utente semplice. Il parametro generico TUser accetta il tipo della classe utente, che in genere è la classe IdentityUser definita. Il parametro generico TKey accetta il tipo di chiave utente.

public class UserStore : IUserStore<IdentityUser, int>
{
    public UserStore() { ... }
    public UserStore(ExampleStorage database) { ... }
    public Task CreateAsync(IdentityUser user) { ... }
    public Task DeleteAsync(IdentityUser user) { ... }
    public Task<IdentityUser> FindByIdAsync(int userId) { ... }
    public Task<IdentityUser> FindByNameAsync(string userName) { ... }
    public Task UpdateAsync(IdentityUser user) { ... }
    public void Dispose() { ... }
}

In questo esempio, il costruttore che accetta un parametro denominato database di tipo ExampleDatabase è solo un'illustrazione di come passare la classe di accesso ai dati. Nell'implementazione di MySQL, ad esempio, questo costruttore accetta un parametro di tipo MySQLDatabase.

All'interno della classe UserStore si usano le classi di accesso ai dati create per eseguire operazioni. Nell'implementazione di MySQL, ad esempio, la classe UserStore ha il metodo CreateAsync che usa un'istanza di UserTable per inserire un nuovo record. Il metodo Insert nell'oggetto userTable è lo stesso metodo illustrato nella sezione precedente.

public Task CreateAsync(IdentityUser user)
{
    if (user == null) {
        throw new ArgumentNullException("user");
    }

    userTable.Insert(user);

    return Task.FromResult<object>(null);
}

Interfacce da implementare durante la personalizzazione dell'archivio utenti

L'immagine successiva mostra altri dettagli sulla funzionalità definita in ogni interfaccia. Tutte le interfacce facoltative ereditano da IUserStore.

Figura che mostra altri dettagli sulla funzionalità definita in ogni interfaccia

  • IUserStore
    L'interfaccia IUserStore<TUser, TKey> è l'unica interfaccia che è necessario implementare nell'archivio utenti. Definisce i metodi per la creazione, l'aggiornamento, l'eliminazione e il recupero degli utenti.

  • IUserClaimStore
    L'interfaccia IUserClaimStore<TUser, TKey> definisce i metodi che è necessario implementare nell'archivio utenti per abilitare le attestazioni utente. Contiene metodi o aggiunta, rimozione e recupero delle attestazioni utente.

  • IUserLoginStore
    IUserLoginStore<TUser, TKey> definisce i metodi che è necessario implementare nell'archivio utenti per abilitare provider di autenticazione esterni. Contiene metodi per aggiungere, rimuovere e recuperare gli account di accesso utente e un metodo per recuperare un utente in base alle informazioni di accesso.

  • IUserRoleStore
    L'interfaccia IUserRoleStore<TKey, TUser> definisce i metodi che è necessario implementare nell'archivio utenti per eseguire il mapping di un utente a un ruolo. Contiene metodi per aggiungere, rimuovere e recuperare i ruoli di un utente e un metodo per verificare se un utente è assegnato a un ruolo.

  • IUserPasswordStore
    L'interfaccia IUserPasswordStore<TUser, TKey> definisce i metodi che è necessario implementare nell'archivio utente per rendere persistenti le password con hash. Contiene metodi per ottenere e impostare la password con hash e un metodo che indica se l'utente ha impostato una password.

  • IUserSecurityStampStore
    L'interfaccia IUserSecurityStampStore<TUser, TKey> definisce i metodi che è necessario implementare nell'archivio utenti per usare un indicatore di sicurezza per indicare se le informazioni sull'account dell'utente sono state modificate. Questo timbro viene aggiornato quando un utente modifica la password o aggiunge o rimuove gli account di accesso. Contiene metodi per ottenere e impostare il timbro di sicurezza.

  • IUserTwoFactorStore
    L'interfaccia IUserTwoFactorStore<TUser, TKey> definisce i metodi da implementare per implementare l'autenticazione a due fattori. Contiene metodi per ottenere e impostare se l'autenticazione a due fattori è abilitata per un utente.

  • IUserPhoneNumberStore
    L'interfaccia IUserPhoneNumberStore<TUser, TKey> definisce i metodi da implementare per archiviare i numeri di telefono dell'utente. Contiene metodi per ottenere e impostare il numero di telefono e se il numero di telefono è confermato.

  • IUserEmailStore
    L'interfaccia IUserEmailStore<TUser, TKey> definisce i metodi da implementare per archiviare gli indirizzi di posta elettronica degli utenti. Contiene metodi per ottenere e impostare l'indirizzo di posta elettronica e se il messaggio di posta elettronica viene confermato.

  • IUserLockoutStore
    L'interfaccia IUserLockoutStore<TUser, TKey> definisce i metodi che è necessario implementare per archiviare informazioni sul blocco di un account. Contiene metodi per ottenere il numero corrente di tentativi di accesso non riusciti, ottenere e impostare se l'account può essere bloccato, ottenere e impostare la data di fine del blocco, incrementando il numero di tentativi non riusciti e reimpostando il numero di tentativi non riusciti.

  • IQueryableUserStore
    L'interfaccia IQueryableUserStore<TUser, TKey> definisce i membri che è necessario implementare per fornire un archivio utenti querybile. Contiene una proprietà che contiene gli utenti su cui è possibile eseguire query.

    Si implementano le interfacce necessarie nell'applicazione; ad esempio, le interfacce IUserClaimStore, IUserLoginStore, IUserRoleStore, IUserPasswordStore e IUserSecurityStampStore, come illustrato di seguito.

public class UserStore : IUserStore<IdentityUser, int>,
                         IUserClaimStore<IdentityUser, int>,
                         IUserLoginStore<IdentityUser, int>,
                         IUserRoleStore<IdentityUser, int>,
                         IUserPasswordStore<IdentityUser, int>,
                         IUserSecurityStampStore<IdentityUser, int>
{
    // interface implementations not shown
}

Per un'implementazione completa (incluse tutte le interfacce), vedere UserStore (MySQL).

IdentityUserClaim, IdentityUserLogin e IdentityUserRole

Lo spazio dei nomi Microsoft.AspNet.Identity.EntityFramework contiene implementazioni delle classi IdentityUserClaim, IdentityUserLogin e IdentityUserRole . Se si usano queste funzionalità, è possibile creare versioni personalizzate di queste classi e definire le proprietà per l'applicazione. Tuttavia, a volte è più efficiente non caricare queste entità in memoria durante l'esecuzione di operazioni di base, ad esempio l'aggiunta o la rimozione dell'attestazione di un utente. Al contrario, le classi dell'archivio back-end possono eseguire queste operazioni direttamente nell'origine dati. Ad esempio, il metodo UserStore.GetClaimsAsync() può chiamare userClaimTable.FindByUserId(user. Id) metodo per eseguire direttamente una query su tale tabella e restituire un elenco di attestazioni.

public Task<IList<Claim>> GetClaimsAsync(IdentityUser user)
{
    ClaimsIdentity identity = userClaimsTable.FindByUserId(user.Id);
    return Task.FromResult<IList<Claim>>(identity.Claims.ToList());
}

Personalizzare la classe del ruolo

Quando si implementa un provider di archiviazione personalizzato, è necessario creare una classe di ruolo equivalente alla classe IdentityRole nello spazio dei nomi Microsoft.ASP.NET.Identity.EntityFramework :

Il diagramma seguente illustra la classe IdentityRole che è necessario creare e l'interfaccia da implementare in questa classe.

Immagine della classe Identity Role

L'interfaccia IRole<TKey> definisce le proprietà che RoleManager tenta di chiamare durante l'esecuzione di operazioni richieste. L'interfaccia contiene due proprietà: ID e Name. L'interfaccia IRole<TKey consente di specificare il tipo di chiave per il ruolo tramite il parametro TKey> generico. Il tipo della proprietà Id corrisponde al valore del parametro TKey.

Il framework Identity fornisce anche l'interfaccia IRole (senza il parametro generico) quando si vuole usare un valore stringa per la chiave.

Nell'esempio seguente viene illustrata una classe IdentityRole che usa un numero intero per la chiave. Il campo ID è impostato su int in modo che corrisponda al valore del parametro generico.

public class IdentityRole : IRole<int>
{
    public IdentityRole() { ... }
    public IdentityRole(string roleName) { ... }
    public int Id { get; set; }
    public string Name { get; set; }
}

Per un'implementazione completa, vedere IdentityRole (MySQL).

Personalizzare l'archivio ruoli

Si crea anche una classe RoleStore che fornisce i metodi per tutte le operazioni sui dati sui ruoli. Questa classe equivale alla classe RoleStore<TRole> nello spazio dei nomi Microsoft.ASP.NET.Identity.EntityFramework. Nella classe RoleStore implementi IRoleStore<TRole, TKey> e facoltativamente l'interfaccia IQueryableRoleStore<TRole, TKey> .

Immagine che mostra una classe dell'archivio ruoli

Nell'esempio seguente viene illustrata una classe dell'archivio ruoli. Il parametro generico TRole accetta il tipo della classe del ruolo, che in genere è la classe IdentityRole definita. Il parametro generico TKey accetta il tipo della chiave del ruolo.

public class RoleStore : IRoleStore<IdentityRole, int>
{
    public RoleStore() { ... }
    public RoleStore(ExampleStorage database) { ... }
    public Task CreateAsync(IdentityRole role) { ... }
    public Task DeleteAsync(IdentityRole role) { ... }
    public Task<IdentityRole> FindByIdAsync(int roleId) { ... }
    public Task<IdentityRole> FindByNameAsync(string roleName) { ... }
    public Task UpdateAsync(IdentityRole role) { ... }
    public void Dispose() { ... }
}
  • IRoleStore<TRole>
    L'interfaccia IRoleStore definisce i metodi da implementare nella classe dell'archivio ruoli. Contiene metodi per la creazione, l'aggiornamento, l'eliminazione e il recupero dei ruoli.

  • RoleStore<TRole>
    Per personalizzare RoleStore, creare una classe che implementa l'interfaccia IRoleStore. È necessario implementare questa classe solo se si vogliono usare ruoli nel sistema. Il costruttore che accetta un parametro denominato database di tipo ExampleDatabase è solo un'illustrazione di come passare la classe di accesso ai dati. Nell'implementazione di MySQL, ad esempio, questo costruttore accetta un parametro di tipo MySQLDatabase.

    Per un'implementazione completa, vedere RoleStore (MySQL).

Riconfigurare l'applicazione per l'uso del nuovo provider di archiviazione

È stato implementato il nuovo provider di archiviazione. A questo punto, è necessario configurare l'applicazione per l'uso di questo provider di archiviazione. Se il provider di archiviazione predefinito è stato incluso nel progetto, è necessario rimuovere il provider predefinito e sostituirlo con il provider.

Sostituire il provider di archiviazione predefinito nel progetto MVC

  1. Nella finestra Gestisci pacchetti NuGet disinstallare il pacchetto EntityFramework di Microsoft ASP.NET Identity . Per trovare questo pacchetto, cercare Identity.EntityFramework nei pacchetti installati.
    Immagine della finestra Pacchetti Nu Get Verrà chiesto se si vuole disinstallare anche Entity Framework. Se non è necessario in altre parti dell'applicazione, è possibile disinstallarlo.

  2. Nel file IdentityModels.cs nella cartella Models eliminare o impostare come commento le classi ApplicationUser e ApplicationDbContext . In un'applicazione MVC è possibile eliminare l'intero file IdentityModels.cs. In un'applicazione Web Forms eliminare le due classi, ma assicurarsi di mantenere la classe helper che si trova anche nel file IdentityModels.cs.

  3. Se il provider di archiviazione si trova in un progetto separato, aggiungere un riferimento all'applicazione Web.

  4. Sostituire tutti i riferimenti a using Microsoft.AspNet.Identity.EntityFramework; con un'istruzione using per lo spazio dei nomi del provider di archiviazione.

  5. Nella classe Startup.Auth.cs modificare il metodo ConfigureAuth per usare una singola istanza del contesto appropriato.

    public void ConfigureAuth(IAppBuilder app)
    {
        app.CreatePerOwinContext(ExampleStorageContext.Create);
        app.CreatePerOwinContext(ApplicationUserManager.Create);
        ...
    
  6. Nella cartella App_Start aprire IdentityConfig.cs. Nella classe ApplicationUserManager modificare il metodo Create per restituire un gestore utenti che usa l'archivio utenti personalizzato.

    public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context) 
    {
        var manager = new ApplicationUserManager(new UserStore(context.Get<ExampleStorageContext>()));
        ...
    }
    
  7. Sostituire tutti i riferimenti a ApplicationUser con IdentityUser.

  8. Il progetto predefinito include alcuni membri nella classe utente che non sono definiti nell'interfaccia IUser; ad esempio Email, PasswordHash e GenerateUserIdentityAsync. Se la classe utente non dispone di questi membri, è necessario implementarli o modificare il codice che usa questi membri.

  9. Se sono state create istanze di RoleManager, modificare tale codice per usare la nuova classe RoleStore.

    var roleManager = new RoleManager<IdentityRole>(new RoleStore(context.Get<ExampleStorageContext>()));
    
  10. Il progetto predefinito è progettato per una classe utente con un valore stringa per la chiave. Se la classe utente ha un tipo diverso per la chiave ,ad esempio un numero intero, è necessario modificare il progetto in modo che funzioni con il tipo. Vedere Modificare la chiave primaria per gli utenti in ASP.NET Identity.

  11. Se necessario, aggiungere la stringa di connessione al file Web.config.

Altre risorse