W 2018 roku weszło RODO. Wszystkie strony działające na terenie Unii Europejskiej (to dotyczy też np. sklepów w USA, na których można kupować mieszkając w UE) muszą mieć odpowiednie mechanizmy zabezpieczające politykę prywatności i dane. O tych mechanizmach jest ten artykuł.
Jeśli masz małe pojęcie o ciasteczkach lub nie znasz ich do końca (nie znasz ich parametrów), przeczytaj ten artykuł.
Co z tym RODO?
Jakiś czas temu na terenie Unii Europejskiej weszło GDPR (po polsku RODO). W skrócie, jeśli chodzi o ciasteczka, użytkownik musi zostać poinformowany o polityce prywatności, a także musi zaakceptować niektóre ciasteczka. Poza tym RODO nakłada obowiązek odpowiedniego przetwarzania danych osobowych, co wiąże się z bezpieczeństwem tych danych, administracją itd. Ale nie o tym nie o tym.
.NET ma już gotowe mechanizmy, które wystarczy podpiąć. Pytanie tylko – czy tego potrzebujesz?
Zaznaczam, że nie jestem prawnikiem. Generalnie jeśli zbierasz jakiekolwiek informacje o użytkowniku za pomocą ciasteczek (chociażby listę rzeczy, które kupił w Twoim sklepie lub ostatnio zakupiony produkt albo też śledzisz jego ruchy na Twojej witrynie), to prawnie powinieneś go o tym poinformować, a on musi na to wyrazić zgodę. Jeśli tego nie zrobisz, to Ty możesz mieć później problemy prawne i płacić kary. Także nie lekceważ tego obowiązku. Większość użytkowników i tak zawsze klika „OK”, nie czytając nawet polityki prywatności. A gotowy mechanizm załatwia wszystko.
Google Analytics i inne aplikacje śledzące
Pamiętaj też, że jeśli używasz google analytics, czy też Smartlook (pokazuje dokładnie co użytkownik robi na Twojej stronie – jak na filmie – polecam), to też musisz o tym poinformować.
Polityka prywatności
Na pierwszy ogień idzie polityka prywatności, którą musisz mieć na swojej stronie. Na szczęście domyślny szablon WebApplication z VisualStudio ma już taką stronę – Privacy.cshtml. Powinieneś tam właśnie wpisać swoją politykę. Pewnie teraz pytanie – skąd to wziąć? Odpowiedź prawilna – skontaktuj się z prawnikiem; odpowiedź nieprawilna – skopiuj z podobnej strony. Ale na BOGA! Przeczytaj ją, zrozum i zmodyfikuj pod swoje potrzeby. I najlepiej daj ją na koniec do przeczytania prawnikowi, niech się wypowie. To Ty jesteś za to odpowiedzialny…
Teraz skonfigurujemy mechanizm wyrażania zgody na ciasteczka. Ta informacja (czy user wyraził zgodę, czy nie) jest zapisywana w… ciasteczku 😉 Ale to specjalne „ciasteczko zgody”, które na stronie MUSI być i jest niezbędne do prawidłowego działania aplikacji (esencjonalne).
W pliku Startup.cs w metodzie ConfigureServices dodaj taki kod:
CookiePolicyOptions ma jeszcze kilka ciekawych elementów:
OnDeleteCookie – akcja wywoływana podczas usuwania ciasteczka
ConsentCookie – parametry ciasteczka, które zapamiętuje zgodę użytkownika na ciasteczka 🙂
OnAppendCookie – akcja wywoływana podczas dodawania ciasteczka
Secure – czy ciasteczka muszą być bezpieczne (CookieOptions.Secure = true)
HttpOnly – czy ciasteczka muszą mieć atrybut HttpOnly
Dodanie polityki do middleware
Następnie w metodzie Configure musisz dodać tę politykę do middleware:
app.UseStaticFiles();
app.UseCookiePolicy();
Dodaj to za UseStaticFiles i przed UseRouting. Właściwie przed jakimkolwiek użyciem ciasteczek śledzących.
Konfiguracja w .NET6
Jeśli używasz .NET6, możesz nie mieć pliku Startup.cs i metod ConfigureServices i Configure. W takim przypadku dodajesz te elementy normalnie w pliku Program.cs analogicznie do innych, np:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
UWAGA!
Pamiętaj, że mechanizm polityki zablokuje tworzenie ciasteczek, jeśli użytkownik nie wyrazi na nie zgody. Podczas tworzenia takiego ciastka, które nie jest oznaczone jako IsEssential zostanie wywołany cichy wyjątek i ciasteczko nie zostanie dołączone do odpowiedzi idącej do przeglądarki. Jeśli jednak masz na stronie ciastka, które niczego nie śledzą, ale są konieczne do poprawnego działania serwisu, oznacz je jako essential: CookieOptions.IsEssential = true. Takie ciastko zostanie zapisane nawet jeśli użytkownik nie wyrazi zgody na śledzenie. Pamiętaj tylko, że te ciastka nie mogą śledzić jego ruchów.
Dodanie informacji do widoku/strony
Teraz musisz dodać informację o ciasteczkach do swoich widoków. Po prostu zmodyfikuj _Layout.cshtml. Odszukaj div z klasą container i dodaj w nim partialview:
Teraz dodaj plik _CookieConsentPartial.cshtml do folderu Views/Shared lub Pages:
@using Microsoft.AspNetCore.Http.Features
@{
var consentFeature = Context.Features.Get<ITrackingConsentFeature>();
var showBanner = !consentFeature?.CanTrack ?? false;
var cookieString = consentFeature?.CreateConsentCookie();
}
@if (showBanner)
{
<div id="cookieConsent" class="alert alert-dark alert-dismissible fade show" role="alert">
Strona używa ciasteczek. <a asp-controller="Home" asp-action="Privacy">Przeczytaj naszą politykę prywatności</a>.
<button type="button" class="accept-policy close" data-dismiss="alert" aria-label="Close" data-cookie-string="@cookieString">
<span aria-hidden="true">Akceptuję</span>
</button>
</div>
<script>
(function () {
var button = document.querySelector("#cookieConsent button[data-cookie-string]");
button.addEventListener("click", function (event) {
document.cookie = button.dataset.cookieString;
}, false);
})();
</script>
}
W tym kodzie nie ma niczego dziwnego (pochodzi z oficjalnej dokumentacji Microsoftu i korzysta z Bootstrapa). Po prostu dopisz tutaj swój komunikat albo skonstruuj własnego diva. Ważne jest to, żeby pokazać tego diva jeśli użytkownik nie wyraził zgody na ciasteczka i nie pokazywać go, gdy wyraził.
Po wciśnięciu przycisku, JavaScript zapisze cookie przesłane w danych tego przycisku (atrybut data-cookie-string). Zauważ, że cały string tworzący cookie otrzymałeś z metody CreateConsentCookie().
Po wyrażeniu zgody w taki sposób (zapisaniu ciasteczka z CreateConsentCookie()), framework już normalnie obsłuży wszystkie Twoje ciastka.
I to właściwie tyle. Jeśli czegoś nie rozumiesz albo znalazłeś błąd w tekście, daj znać w komentarzu
Skoro tu jesteś to pewnie używałeś ciasteczek, być może nie do końca świadomie albo nie wiedząc o pewnych niuansach. W tym artykule wyjaśnię czym dokładnie są te ciasteczka i opiszę wszystkie zawiłości, na jakie kiedykolwiek trafiłem. Więc nawet jeśli używasz ciasteczek, ten artykuł może Ci trochę rozjaśnić i odpowiedzieć na kilka pytań, które bałeś się zadać.
Czym są ciasteczka
Ciasteczko to nic innego jak dane przechowywane na komputerze użytkownika. To string składający się z pary klucz=wartość i kilku atrybutów. Ciasteczko zazwyczaj (w zależności od przeglądarki) jest fizycznie reprezentowane jako plik. Każde ciasteczko ma swoją nazwę (klucz). Powoduje to, że do jego zawartości możemy się dobrać właśnie przez nazwę w taki sposób (pseudokod):
string dane = GetCookieByName("moje_ciastko");
SetCookieByName("moje_ciastko", "całkiem nowe dane");
Ciasteczko ma też kilka właściwości jak np. data ważności (expire date). Ale o tym później.
Ma też ograniczenie co do ilości danych – w zależności od przeglądarki, ale załóż, że maks to 4 kB.
Ciasteczka są przesyłane z klienta do serwera i na odwrót za pomocą nagłówków HTTP. Więc staraj się, żeby jednak były małe. I staraj się, żeby nie było ich zbyt dużo.
Teraz możesz zadać pytanie – „ale jak to przesyłane w nagłówku, skoro są na komputerze użytkownika?”
No tak, ale z każdym żądaniem (np. żądanie wyświetlenia strony) przeglądarka wysyła do serwera wszystkie ciastka dla danej strony. Serwer też może wysłać w odpowiedzi na żądanie specjalny nagłówek, który spowoduje, że przeglądarka zapisze ciastko. O tym wszystkim już za chwilę.
Po co ciasteczka?
Wszystko rozbija się o to, że HTTP jest protokołem bezstanowym. Oznacza to, że pomiędzy dwoma wyświetleniami strony nie zachowuje się żaden stan – nie można przechować zmiennych. One są niszczone za każdym razem. Nie można nawet sprawdzić, kto jest zalogowany. Trzeba było ogarnąć jakiś sposób na zarządzanie stanem w aplikacjach internetowych. Jednym z tych sposobów są ciasteczka.
Czym się różni sesja od ciasteczka?
W dużym skrócie sesje też są zbiorem danych. Działają podobnie do ciasteczek, tylko są zapisywane na serwerze. Ciasteczka natomiast zapisują się na komputerze użytkownika. Sesje mogą zależeć od ciasteczek (np. ciasteczko może przechowywać id sesji), ale nie na odwrót. Co więcej sesja kończy się w momencie wylogowania, natomiast ciasteczko – gdy skończy mu się okres ważności (może to być tak długie jak kilka lat albo tak krótkie jak otwarcie przeglądarki). Sesje nie mają też żadnego narzuconego ograniczenia co do ilości danych.
Ciasteczka trwałe i nietrwałe
Ciasteczka mogą być trwałe (persistent) lub nietrwałe (non-persistent). Trwałe ciasteczko jest zapisywane w pliku na dysku użytkownika lub w bazie przeglądarki. Nietrwałe ciasteczka istnieją tylko w pamięci przeglądarki. Nazywa się je również „ciasteczkami sesyjnymi”. Takie ciasteczka tworzy się nie nadając im daty ważności. Czyli ich życie kończy się wraz z zamknięciem przeglądarki.
Tworzenie ciasteczka
Ciasteczko może zostać utworzone przez klienta, jak również przez serwer (to pewien skrót myślowy). W tym drugim przypadku serwer w odpowiedzi na żądanie wysyła nagłówek (Set-Cookie) z ciasteczkiem, który jest odczytywany przez przeglądarkę i ona piecze takie ciasteczko.
W pierwszym przypadku ciasteczko jest zapisywane przez… ehhh… JavaScript.
Teraz będziemy robić kody. Stwórz sobie jakiś projekt testowy, niech to będzie domyślne Asp NetCore WebApp (MVC lub RazorPages) z VisualStudio.
Ciasteczko w JavaScript
Otwórz plik Index.cshtml. Dodaj tam przycisk, który zapisze ciasteczko:
Kod jest bardzo prosty – wciśnięcie przycisku odpala funkcję w JavaScript, która ustawia ciasteczko. Nazwa tego ciasteczka (klucz) to username, a wartość „Adam”. Równie dobrze mógłby tam być cały obiekt zapisany w JSON.
Uruchom teraz ten przykład, ale nie wciskaj jeszcze guzika. Uruchom narzędzia dla developerów w swojej przeglądarce. Ja używam FireFoxa i do tego jest skrót Ctrl + Shift + I. Jeśli nie używasz FireFoxa, w innych przeglądarkach te narzędzia są podobne, więc nie będziesz miał raczej problemu. Tutaj ciasteczka są na karcie DANE.
Spójrz na zawartość ciasteczek w tym oknie:
Widzisz tutaj jakieś 4 ciasteczka na „dzień dobry”. Pochodzą z .NET, nie zajmujmy się nimi teraz.
Wciśnij teraz przycisk, który dodałeś na stronie i zobacz, co się stanie. Powstało nowe ciasteczko o nazwie username:
To ciasteczko będzie żyło aż do zamknięcia przeglądarki. Możesz mu podać też expire date, który usunie konkretne ciasteczko (jeśli data będzie w przeszłości) lub nada mu konkretny czas życia. Wszystko to jest dokładnie opisane na w3schools więc nie będę się rozwodził na temat JavaScriptu więcej 😉
Ciasteczko w .NET
JavaScript jest o tyle miłe, że działa na kliencie. To znaczy, że może utworzyć ciasteczko bezpośrednio na Twoim komputerze. .NET jednak działa na serwerze, co nam daje trochę więcej komunikacji między klientem a serwerem. Czasem też nie da się inaczej:
klient musi wysłać żądanie do serwera (np. GET http://moja-strona.pl)
serwer musi odebrać to żądanie, przetworzyć je i odpowiedzieć na nie, wysyłając ciasteczko
przeglądarka odbierze ciasteczko i zapisze je na dysku lub w swojej bazie.
Zróbmy teraz te wszystkie kroki za pomocą małego formularza. W pliku index.cshtml stwórz prostą formatkę:
Następnie stwórz odpowiednią akcję w kontrolerze (analogicznie to będzie w Razor Pages). W pliku HomeController.cs dopisz metodę Index z metodą POST – to tutaj zostanie wysłany formularz:
Spójrz jak to teraz wygląda. Po kliknięciu przycisku, wysyłane jest żądanie z formularzem na serwer. Na serwerze odczytujemy wartość formularza i do response’a (czyli odpowiedzi, którą generujemy dla klienta) dodajemy nowe ciasteczko. Przeglądarka po otrzymaniu takiej odpowiedzi (z ciasteczkiem) tworzy je fizycznie.
Odczyt ciasteczka
Ciasteczko możemy odczytać też za pomocą JavaScript lub .NET. Jednak JavaScript dostaje wszystkie ciastka dla danej strony, więc sami sobie je musimy parsować. W .NET już to jest zrobione normalnie. Musimy tylko odczytać je na serwerze podczas żądania.
Pamiętaj, że otrzymujesz tylko swoje ciasteczka. Tzn. przeglądarka zwróci ciasteczka tylko dla konkretnej domeny – dla tej, która je utworzyła (z małym wyjątkiem – o tym później). Czyli jeśli wysyłasz żądanie do strony example.com, przeglądarka doda do nagłówków ciasteczka utworzone przez example.com.
Zmień zatem metodę Index (tę domyślną) w taki sposób, aby odczytać to ciasteczko:
public IActionResult Index()
{
var userName = HttpContext.Request.Cookies["username_fromnet"];
ViewData["userName"] = userName;
return View();
}
Zwróć uwagę, że tym razem odczytujemy ciastko z HttpContext.Request – czyli z żądania, które idzie od klienta do serwera. Zapisujemy ciasteczko w odpowiedzi na to żądanie, czyli w HttpContext.Response.
Gdy użytkownik uruchamia aplikację, idzie żądanie do serwera (wraz z wszystkimi ciasteczkami odczytanymi przez przeglądarkę) i wchodzi do metody Index. Stąd odczytujemy sobie konkretne ciasteczko i przekazujemy jego wartość do danych widoku. Na koniec pokazujemy widok, który lekko się zmienił:
Pobieramy dane z ViewData do zmiennej userName. Jeśli teraz ta zmienna nie ma żadnej wartości, to wyświetlamy formularz. Jeśli ma – wyświetlamy powitanie.
Parametry ciasteczka
Jak pisałem wyżej, ciasteczko może mieć swoje parametry. Klasą, która je opisuje jest CookieOptions:
CookieOptions.Expires
Określa czas życia ciasteczka. Zazwyczaj po prostu dodaje się jakąś datę do aktualnej, np. DateTime.Now.AddDays(30). Ciasteczko zostanie usunięte po tej dacie. Co jednocześnie powoduje, że jeśli podasz datę wcześniejszą niż aktualna, ciasteczko zostanie usunięte natychmiast. Pamiętaj, że na serwerze możesz mieć inną datę niż na komputerze użytkownika. Więc ostrożnie z tym.
CookieOptions.MaxAge
Działa podobnie do Expires. Też określa czas życia ciasteczka z tą różnicą, że nie podajesz daty zakończenia życia, tylko jego czas, np: MaxAge = TimeSpan.FromDays(30) – takie ciasteczko po 30 dniach od utworzenia zostanie usunięte. Jest to nowsza, lepsza i bardziej wygodna opcja niż Expires.
CookieOptions.Domain
Domyślnie ciasteczko należy do domeny, która je utworzyła. Czyli jeśli utworzysz ciasteczko z domeny example.com, zostanie ono odczytane zarówno dla domeny example.com, jak i SUBDOMENY www – www.example.com. Jeśli jednak ciasteczko zostanie utworzone z subdomeny www – www.example.com, nie będzie widoczne z domeny example.com. Dlatego też powinieneś skonfigurować domenę na domenę główną, np: CookieOptions.Domain = ".example.com" (kropka na początku) To spowoduje, że ciasteczko będzie dostępne zarówno z domeny głównej jak i z wszystkich subdomen (w szczególności „www”). Jeśli więc masz problem, bo raz ciasteczko działa a raz nie, to pewnie dlatego, że raz Twoja strona jest uruchamiana z subdomeny (www.example.com), a raz nie. Przyjrzyj się temu.
Pamiętaj, że „www” jest subdomeną. Takich subdomen możesz mieć wiele, np: mail.example.com, dev.example.com, git.example.com… Ale chciałbyś, żeby ciasteczka działały tylko na subdomenie www i domenie głównej. Jak to zrobić?
Nie znalazłem na to odpowiedzi, a wszystkie moje testy się nie powiodły. Jeśli masz pomysł, koniecznie podziel się w komentarzu. Z mojej wiedzy wynika, że można mieć ciasteczko albo dla wszystkich subdomen i domeny głównej, albo dla jednej subdomeny, albo dla domeny głównej.
CookieOptions.Path
Podobnie do Domain. Z tą różnicą, że tutaj chodzi o ścieżkę w adresie. Domyślnie Path jest ustawiane na „/”, co oznacza, że ciasteczko będzie dostępne dla wszystkich podstron/routów z Twojego serwisu. Jeśli jednak ustawisz np. na "/login/" oznacza to, że ciasteczko będzie dostępne tylko ze ściezki "login" i dalszych, np: www.example.com/login, www.example.com/login/facebook
CookieOptions.HttpOnly
To specjalny rodzaj ciastka mający na celu zapobieganie pewnym atakom (np. XSS – Cross site scripting). Oczywiście nie polegaj na tym w 100%. Generalnie chodzi o to, że ciastka z takim atrybutem nie mogą (nie powinny) być odczytywane przez JavaScript. Po prostu document.cookies nie zwróci takiego ciastka. Możesz jedynie odczytać je na serwerze – HttpContext.Request.Cookies.
CookieOptions.Secure
Jeśli ustawione na true, ciasteczko zostanie wysłane z przeglądarki do serwera tylko wtedy, jeśli komunikacja odbywa się po HTTPS.
CookieOptions.SameSite
Ten parametr odpowiada za bezpieczeństwo ciasteczek. Ciastka są z natury podatne na pewne ataki. Atrybut SameSite ma tą podatność zmniejszyć. Jak?
Wyobraź sobie dwie strony. Twoja – www.example.com i jakaś inna – www.abc.com.
Na stronie www.abc.com znajduje się ramka (iframe), do której ładowana jest Twoja strona. A więc ze strony www.abc.com idzie żądanie do Twojej. W tym momencie przeglądarka odczytuje Twoje ciasteczka i wysyła je do strony www.abc.com.
Możesz teraz zrobić prosty test. Poniżej masz przycisk i ramkę. Otwórz narzędzia deweloperskie (Shift + Ctrl + I) i przejdź na zakładkę „Sieć” (Network). Teraz wciśnij poniższy przycisk (Załaduj Google do ramki) i zobacz, co się dzieje w „sieci”. Poszło żądanie do Google wraz z odpowiedziami – co więcej niektóre odpowiedzi zawierają ciasteczka (to nagłówki „Set-Cookie”)
Wyobraź sobie teraz taką sytuację, że masz stronę, na której ktoś jest zalogowany. Id sesji znajduje się w zwykłym ciasteczku. Teraz, jeśli taki zalogowany użytkownik otworzy tak spreparowaną stronę, ta strona dostanie to właśnie ciasteczko z id jego sesji. Dzięki temu strona www.abc.com będzie mogła dobrać się do sesji zalogowanego użytkownika w Twoim serwisie i w jego imieniu wykonywać operacje. To tak z grubsza. Taki atak nazywa się CSRF (Cross site request forgery).
Przeglądarki nie rozróżniają kto wysłał żądanie – użytkownik, czy inny skrypt. Teraz z pomocą przychodzi atrybut SameSite.
SameSite w .NET może przyjąć 4 wartości:
Unspecified
None
Lax
Strict
Wartość STRICT
Ustawienie SameSite na strict spowoduje, że jeśli żądanie przyjdzie z innej domeny niż ta ustawiona w ciasteczku, ciastko nie zostanie dołączone do odpowiedzi.
Wartość LAX
Jeśli żądanie idzie „bezpieczną” metodą (np. GET) i dodatkowo zmieni się adres w przeglądarce, to wtedy ciasteczka zostaną wysłane.
Wartość NONE
Pozwala na przekazywanie ciasteczek pomiędzy stronami bez żadnych restrykcji.
Wartość UNSPECIFIED
Atrybut w ogóle nie zostanie dołączony do ciasteczka, co spowoduje domyślne zachowanie przeglądarki.
Domyślnie wszystkie ciasteczka w .NET są ustawione na SameSite = Lax.
Oznacza dane ciastko jako niezbędne do funkcjonowania strony. Takie ciastko nie może śledzić poczynań użytkowników.
To właściwie tyle jeśli chodzi o ciastka. Będziesz ich jeszcze używał do automatycznego logowania użytkownika, ale to już temat na inny artykuł, który napiszę wkrótce. Także zapisz się na newsletter, żeby go nie pominąć 🙂
Obsługujemy pliki cookies. Jeśli uważasz, że to jest ok, po prostu kliknij "Akceptuj wszystko". Możesz też wybrać, jakie chcesz ciasteczka, klikając "Ustawienia".
Przeczytaj naszą politykę cookie