Preferences ja SecureStorage .NET MAUI -sovelluksessa: arkaluonteisen datan turvallinen tallennus

Opi milloin käyttää Preferences- ja SecureStorage-luokkia .NET MAUI -sovelluksessa. Opas kattaa alustakohtaisen salauksen (iOS Keychain, Android Keystore), token-hallinnan MVVM-arkkitehtuurissa sekä yleisimmät virhetilanteet ja parhaat käytännöt.

Preferences ja SecureStorage .NET MAUI: turvallinen tallennus
On this page
  1. Preferences-luokka: asetukset ja sovelluksen tila
  2. SecureStorage-luokka: arkaluonteinen data suojattuun säilöön
  3. Alustakohtainen toteutus: Keychain ja Android Keystore
  4. Autentikointitoken-palvelu MVVM-rakenteessa
  5. Rajoitukset ja parhaat käytännöt

Preferences ja SecureStorage ovat .NET MAUI:n kaksi valmista mekanismia paikalliseen avain-arvo-tallennukseen: ensimmäinen sopii käyttäjäasetuksiin ja sovelluksen tilaan, toinen suojaa arkaluonteiset tiedot — tokenit, salasanat ja API-avaimet — alustakohtaisen salauksen taakse. Oikea valinta näiden välillä vaikuttaa suoraan sovelluksen tietoturvaan.

Tässä oppaassa käydään läpi molempien luokkien API ja toimintalogiikka, selitetään miten salaus toimii iOS:llä (Keychain) ja Androidilla (Android Keystore), sekä rakennetaan käytännön esimerkki autentikointitoken-palvelusta MVVM-arkkitehtuurissa. Artikkelin lopussa käsitellään yleisimmät virhetilanteet ja parhaat käytännöt.

Preferences-luokka: asetukset ja sovelluksen tila

Preferences on yksinkertainen avain-arvo-varasto, joka toimii Androidilla SharedPreferences-mekanismin päällä ja iOS:llä NSUserDefaults-pohjaisen tallennuksen kautta. Se soveltuu käyttötapauksiin, joissa data ei ole arkaluonteista:

  • käyttöliittymäasetukset, kuten teema tai kieli
  • viimeksi valittu välilehti tai näkymä
  • onboarding-tilan seuranta
  • ei-arkaluonteiset käyttäjäkohtaiset valinnat, kuten ilmoitusasetukset

Perusoperaatiot ovat synkronisia:

// Tallentaminen
Preferences.Default.Set("theme", "dark");
Preferences.Default.Set("notifications_enabled", true);
Preferences.Default.Set("last_sync_ticks", DateTime.UtcNow.Ticks);

// Lukeminen oletusarvolla
string theme = Preferences.Default.Get("theme", "light");
bool notificationsEnabled = Preferences.Default.Get("notifications_enabled", true);

// Avaimen olemassaolon tarkistus
if (Preferences.Default.ContainsKey("onboarding_complete"))
{
    // onboarding on suoritettu
}

// Yhden avaimen poistaminen
Preferences.Default.Remove("theme");

// Kaiken tyhjentäminen
Preferences.Default.Clear();

Tuetut tietotyypit ovat bool, int, long, double, float, string ja DateTime. Jos tallennat muun tyyppisen arvon, saat käännösaikaisen virheen.

Preferences tukee myös osastoja sharedName-parametrin kautta, joiden avulla saman sovelluksen eri moduulit voivat käyttää erillisiä nimiavaruuksia ilman avainten törmäysriskiä:

Preferences.Default.Set("token_version", 2, "auth_module");
int version = Preferences.Default.Get("token_version", 0, "auth_module");

SecureStorage-luokka: arkaluonteinen data suojattuun säilöön

SecureStorage on tarkoitettu nimenomaan arkaluonteiselle datalle. Tyypillisiä käyttötapauksia ovat autentikointitokenit (access token ja refresh token), API-avaimet, joita ei voi kierrättää lyhyessä ajassa, sekä muut salaisuudet, jotka eivät saa tallentua selkokielisenä laitteelle.

Kaikki operaatiot ovat asynkronisia:

// Tallentaminen
await SecureStorage.Default.SetAsync("access_token", tokenValue);

// Lukeminen — palauttaa null, jos avainta ei löydy
string? token = await SecureStorage.Default.GetAsync("access_token");

// Yhden avaimen poistaminen (synkroninen)
SecureStorage.Default.Remove("access_token");

// Kaiken poistaminen (synkroninen)
SecureStorage.Default.RemoveAll();

GetAsync palauttaa null-arvon, jos avainta ei löydy — se ei heitä poikkeusta. Palautusarvo on aina tarkistettava ennen käyttöä, jotta vältetään NullReferenceException.

Jos sovellus hallinnoi sekä access tokenia että refresh tokenia, tallenna ne erillisinä avaimina eikä samaan JSON-merkkijonoon. Näin yksittäinen token voidaan päivittää tai poistaa ilman, että toisen arvo ylikirjoitetaan.

Alustakohtainen toteutus: Keychain ja Android Keystore

Ymmärtämällä, mitä alustan alla tapahtuu, pystyt arvioimaan turvallisuustason realistisesti ja tarvittaessa selittämään sen auditoijalle tai tietoturvatiimille.

iOS ja macOS

iOS:llä SecureStorage käyttää Apple Keychain Services -palvelua. Keychain on laitteistotasolla suojattu varasto, jonka pääsykontrollia hallinnoidaan uudemmilla laitteilla Secure Enclave -piirillä. Tallennettuihin arvoihin pääsee oletuksena vain oma sovellus; Keychain Access Groups -mekanismilla voidaan jakaa tietoja saman kehittäjätilin sovellusten kesken, mutta se edellyttää natiivia konfigurointia.

Keychain-tallennus on kytköksissä sovelluksen Bundle Identifier -arvoon. iOS säilyttää Keychain-tiedot oletuksena sovelluksen poistamisen jälkeen: jos sovellus reinstalloidaan, vanhat tiedot ovat edelleen saatavilla. Tämä voidaan kiertää poistamalla arkaluonteiset avaimet SecureStorage.Default.RemoveAll()-kutsulla sovelluksen ensimmäisellä käynnistyskerralla tuoreessa asennuksessa, esimerkiksi tarkistamalla Preferences-tallennetusta lipusta, onko asennus uusi.

Android

Androidilla SecureStorage käyttää EncryptedSharedPreferences-luokkaa, joka on osa Jetpack Security -kirjastoa. Salausavain generoidaan ja tallennetaan Android Keystore -järjestelmään — prosessista erilliseen suojattuun komponenttiin, josta avainta ei pysty poimimaan sovelluksen prosessissa. Android API 23 (Marshmallow) ja uudemmat versiot tukevat Android Keystore -pohjaista salausta täysipainoisesti; varmista projektin MinimumAndroidVersion-asetus tätä vasten.

Windows (WinUI 3)

Windows-kohteella SecureStorage käyttää DataProtectionProvider-rajapintaa Windows DPAPI:n kautta. Data on sidottu käyttäjäprofiiliin, ei laitteen laitteistoon, joten turvallisuustaso on matalampi kuin iOS:llä tai Androidilla. Jos sovellus kohdistuu myös Windowsiin ja tietoturvavaatimukset ovat korkeat, harkitse lisäsuojaustasoa Windows-kohtaisessa toteutuksessa.

Autentikointitoken-palvelu MVVM-rakenteessa

Hyvä käytäntö on piilottaa SecureStorage-kutsut palveluluokan taakse, jolloin ViewModel ei ole suoraan riippuvainen MAUI-kohtaisesta API:sta. Tämä helpottaa yksikkötestausta — testissä voidaan injektoida mock-toteutus — ja pitää liiketoimintalogiikan irrallaan alustariippuvaisesta koodista.

Ensin määritellään rajapinta:

public interface ITokenService
{
    Task<string?> GetAccessTokenAsync();
    Task SaveAccessTokenAsync(string token);
    Task<string?> GetRefreshTokenAsync();
    Task SaveRefreshTokenAsync(string token);
    Task ClearAllTokensAsync();
}

Toteutus SecureStorage-luokan päällä:

public class TokenService : ITokenService
{
    private const string AccessTokenKey = "auth_access_token";
    private const string RefreshTokenKey = "auth_refresh_token";

    public Task<string?> GetAccessTokenAsync() =>
        SecureStorage.Default.GetAsync(AccessTokenKey);

    public Task SaveAccessTokenAsync(string token) =>
        SecureStorage.Default.SetAsync(AccessTokenKey, token);

    public Task<string?> GetRefreshTokenAsync() =>
        SecureStorage.Default.GetAsync(RefreshTokenKey);

    public Task SaveRefreshTokenAsync(string token) =>
        SecureStorage.Default.SetAsync(RefreshTokenKey, token);

    public Task ClearAllTokensAsync()
    {
        SecureStorage.Default.Remove(AccessTokenKey);
        SecureStorage.Default.Remove(RefreshTokenKey);
        return Task.CompletedTask;
    }
}

Rekisteröi palvelu MauiProgram.cs-tiedostossa riippuvuusinjektiokonttiin:

builder.Services.AddSingleton<ITokenService, TokenService>();

ViewModelissa palvelua käytetään konstruktori-injektion kautta. Alla esimerkki, jossa CommunityToolkit.Mvvm:n [RelayCommand]-attribuuttia käytetään kirjautumis- ja uloskirjautumislogiikan kytkemiseen:

public partial class LoginViewModel : ObservableObject
{
    private readonly ITokenService _tokenService;

    public LoginViewModel(ITokenService tokenService)
    {
        _tokenService = tokenService;
    }

    [RelayCommand]
    private async Task LoginAsync()
    {
        // Autentikointikutsu omaan auth-palveluun
        string receivedToken = await GetTokenFromServerAsync();
        await _tokenService.SaveAccessTokenAsync(receivedToken);
    }

    [RelayCommand]
    private async Task LogoutAsync()
    {
        await _tokenService.ClearAllTokensAsync();
    }
}

Tämän rakenteen ansiosta yksikkötesteissä voidaan injektoida ITokenService-mock ilman, että testit tarvitsevat pääsyä laitteen Keychainiin tai Android Keystoreen.

Rajoitukset ja parhaat käytännöt

Virheenkäsittely

SecureStorage voi heittää SecureStorageNotSupportedException-poikkeuksen laitteilla, joilla laitteen salaus ei ole käytössä. Vaikka tällaisia laitteita on nykyisin vähän, virheenkäsittely on tärkeää sovelluksen vakauden kannalta:

try
{
    await SecureStorage.Default.SetAsync("key", "value");
}
catch (SecureStorageNotSupportedException)
{
    // Ilmoita käyttäjälle, että laitteen salaus on kytkettävä päälle
}

Suorituskyky ja välimuisti

SecureStorage-operaatiot ovat hitaampia kuin tavalliset Preferences-kutsut, koska kryptografisia operaatioita suoritetaan taustalla. Kutsuja ei pidä tehdä jokaisella näkymän renderöinnillä tai navigoinnilla. Parempi lähestymistapa on hakea token SecureStoragesta kerran sovelluksen käynnistyessä tai kirjautumisen jälkeen, tallentaa se palveluluokan muistiin ja käyttää muistissa olevaa arvoa suorituksen aikana.

Mitä ei pidä tallentaa SecureStorageen

SecureStorage on optimoitu pienille merkkijonoille. Siihen ei pidä tallentaa suuria datamääriä tai JSON-tietorakenteita — käytä sen sijaan salattua SQLite-tietokantaa rakenteistetulle paikalliselle datalle. Binääridataa SecureStorage ei tue lainkaan, sillä se käsittelee ainoastaan merkkijonoja. Usein vaihtuville tiedoille muistissa oleva välimuisti on oikea ratkaisu.

Avainten nimeäminen

Käytä vakioita tai enum-pohjaista rakennetta avainten nimille — näin vältetään kirjoitusvirheistä johtuvat vaikeat debugata-ongelmat. Aiemmassa TokenService-esimerkissä avaimet on määritelty private const string -muuttujina: tämä on suositeltava käytäntö kaikissa projekteissa riippumatta tallennusmekanismista.

Usein kysytyt kysymykset

Voiko SecureStorageen tallentaa suuria datamääriä?

Ei suositella. SecureStorage on optimoitu pienille merkkijonoille, kuten tokeneille ja salasanoille. Suuremmille tietomäärille kannattaa harkita salattua SQLite-tietokantaa, joka tarjoaa myös rakenteisen tietomallinnuksen.

Mitä tapahtuu SecureStorage-tiedoille, kun sovellus poistetaan iOS:llä?

iOS:llä Keychain-tiedot säilyvät oletuksena sovelluksen poistamisen jälkeen. Jos tämä ei ole haluttua, sovelluksen tulisi tunnistaa tuore asennus ja kutsua SecureStorage.Default.RemoveAll() ensimmäisellä käynnistyskerralla. Androidilla tiedot poistetaan sovelluksen poistamisen yhteydessä automaattisesti.

Toimiiko SecureStorage Android-emulaattorissa?

Useimmissa tapauksissa kyllä. Jos emulaattorin PIN tai salasana ei ole asetettu, joillakin API-tasoilla voi esiintyä SecureStorageNotSupportedException. Aseta lukitusnäyttö emulaattorin asetuksista ennen testaamista.

Pitääkö access token tallentaa SecureStorageen, vai riittääkö muistissa pitäminen?

Lyhytelikaisia access tokeneja voi pitää muistissa sovelluksen ajonaikaisessa tilassa — näin ne katoavat sovelluksen sulkeutuessa. Pitkäikäiset refresh tokenit, jotka myöntävät oikeuden uusiin access tokeneihin, tulee ehdottomasti tallentaa SecureStorageen.

Voinko lisätä oman salauksen Preferences-datan päälle?

Teknisesti kyllä, käyttämällä System.Security.Cryptography-nimiavaruuden luokkia ennen tallentamista. Tämä lisää kuitenkin toteutuksen monimutkaisuutta merkittävästi. SecureStorage on oikea työkalu silloin, kun alustakohtainen salaus riittää — omaa salaustoteutusta tarvitaan harvoin.

Article changelog (1)
  • — Content revised