Autentykacja przy użyciu tokena
[info] Jeśli pracujesz na laboratorium, aby rozpocząć realizację kolejnego modułu, musisz wykonać poniższe operacje
sklonuj repozytorium ze swoim kodem (
git clone ŚCIEŻKA_DO_REPOZYTORIUM),zainstaluj potrzebne gemy (
bundle install --path vendor/bundle),dokonaj migracji bazy danych (
rails db:migrate)uruchom serwer (
rails server)
Autentykacja użytkownika w naszej aplikacji jest dotąd realizowane przez ciasteczko sesji, które przeglądarka uzyskuje od serwera po wysłaniu mu w formularzu odpowiednich danych logowania. W wypadku komunikacji z poziomu interfejsu programistycznego, wygodniejszym rozwiązaniem jest uzyskanie w tym samym procesie tak zwanego tokena, który będziemy mogli dodawać jako nagłówek do każdego zapytania wymagającego autentykacji.
Dla wersji 5 frameworka Ruby on Rails jest to operacja relatywnie łatwa i zbliżona do procesu dodawania do użytkownika bezpiecznego hasła. W podobny sposób dodajemy więc do modelu studenta (Student.rb) informację o wyposażeniu go w token.
has_secure_tokenNiestety, metoda usuwania tokena nie jest domyślne zaimplementowana. Szalenie przyda się ona przy wylogowywaniu (deautentykacji) użytkownika, więc zaimplementujmy ją w modelu studenta ręcznie.
def invalidate_token
self.update_columns(token: nil)
endDeklaracja istnienia tokena jeszcze nie dodaje go do bazy, więc musimy stworzyć odpowiednią migrację.
rails generate migration AddTokenToStudents token:string
class AddTokenToStudents < ActiveRecord::Migration[5.1]
def change
add_column :students, :token, :string
add_index :students, :token
end
endNie zapomnijmy o zmigrowaniu bazy.
rails db:migrate
Logowanie jako uzyskiwanie tokena
Teoretycznie moglibyśmy w tej chwili stworzyć nowy kontroler, odpowiedzialny wyłącznie za uzyskiwanie i deautentykowanie tokena. Jeśli jednak spojrzymy na niego jak na odpowiednik ciasteczka przewidziany dla innej reprezentacji danych (json zamiast html), możemy utożsamić uzyskiwanie tokena z logowaniem, a pozbywanie się go z wylogowywaniem. Możliwa więc będzie wspólna implementacja dla wszystkich reprezentacji w kontrolerze sesji.
Poprawiamy metodę create w kontrolerze sesji, aby jej zachowanie zależało od oczekiwanej przez klienta reprezentacji. Chwilowo uznajmy, że przy html zachowywać się będzie tak samo jak dotąd, a przy wymaganym json nie będzie wykonywać żadnej logiki poza wysłaniem odpowiedzi w formacie json z komunikatem o próbie logowania.
Przy generowaniu formularzy, Ruby on Rails domyślnie ukrywa w nich parametr, który weryfikuje później po wysłaniu, aby upewnić serwer, że dane pochodzą z wygenerowanego w widoku formularza. W wypadku danych wysłanych bezpośredniu do API, nie dysponujemy tym parametrem. Aby nie kłopotać się tym w tej chwili, wyłączamy zabezpieczenie w kontrolerze.
Spróbujmy wysłać zapytanie z próbą logowania.
curl -X POST -d '{"index": 123456, "password": "haslo123"}' -H "Accept: application/json" -H "Content-Type: application/json" http://localhost:3000/login
Otrzymamy zaplanowany plik json, w zależności od poprawności danych.
lub
Przy wprowadzeniu poprawnych danych, zamiast komunikatu, powinniśmy otrzymać token. Poprawmy więc kod, renderując plik JSON z jednoelementowym słownikiem zawierającym token.
Wyślijmy raz jeszcze poprawne zapytanie logowania.
Otrzymujemy token, ale jest on pusty. To całkowicie logiczne, ponieważ jeszcze go nie wygenerowaliśmy. Będzie to jednak możliwe jedynie, jeśli po autentykacji, do obiektu student przypiszemy hasło. Wydaje się to nielogiczne, ale cóż, inaczej walidator hasła odrzuci zapytanie i wygeneruje wewnętrzny błąd aplikacji.
Po kolejnej próbie autentykacji powinniśmy już otrzymać token. Udało się nam obsłużyć generowanie tokenów.
Autentykacja przy użyciu tokena
Tokeny mają służyć nam za narzędzie do autentykacji użytkowników. Musimy więc być w stanie określić, które metody wymagają podania go, a które nie. Uzupełnijmy więc helper sesji o metodę require_token, która będzie określać to wymaganie.
W wypadku, kiedy żądanym formatem jest html, nie dzieje się nic szczególnego. W wypadku, kiedy wymaganym formatem jest json, uruchamiamy komendę authenticate_token, a jeśli niczego nie zwróci, komendę render_unauthorized z odpowiednim komunikatem błędu.
Potrzebujemy więc jeszcze dwóch metod. Najpierw authenticate_token, która weryfikuje token podany w nagłówku zapytania i jeśli znajdzie jakieś dopasowanie wśród studentów, ustawia odpowiednio pole @current_student.
Drugą metodą jest render_unauthorized(), przyjmujące żądany komunikat błędu i renderujące go użytkownikowi.
Dokonajmy logowania i zanotujmy sobie token.
curl -X POST -d '{"index": 123456, "password": "haslo123"}' -H "Accept: application/json" -H "Content-Type: application/json" http://localhost:3000/login
Potrzebujemy jeszcze akcji, którą zabezpieczymy autentykacją. Na te potrzeby, stworzymy nową metodę kontrolera static. Będzie to metoda feed, listująca nam wszystkie informacje o kursach, do których zapisany jest autentykowany student. Rozpoczniemy od odpowiedniego wpisu w routes.
Następnie uzupełnimy kontroler static o pustą metodę feed.
Spróbujmy przygotować widok app/views/static/feed.html.erb, w którym zmieścimy pożądane informacje w formacie html..
Iterujemy wszystkie kursy aktualnie zalogowanego studenta. Wyświetlamy nazwę kursu i iterjujemy wszystkie tematy. Wyświetlamy tytuł tematu i listujemy wszystkie posty, wyświetlając ich ciała. Upewnijmy się, że jesteśmy zalogowani i odwiedźmy adres http://localhost:3000/feed.

Faktycznie udało nam się przygotować widok, którego oczekiwaliśmy.
Zajmiemy się teraz przygotowaniem widoku w formacie json. Posłuży nam do tego jbuilder. Utwórzmy pusty plik app/views/static/feed.json.jbuilder. Nie zmieniając niczego odwiedźmy http://localhost:3000/feed.json.
Wygenerował się pusty słownik w formacie json. Możemy teraz Spróbujmy więc odtworzyć widok, który stworzyliśmy już w html, tym razem w formacie json.
Zacznijmy od małych kroków. Wypełnianie słownika JSON działa na poniższej zasadzie.
Jeśli wypełnimy tak nasz widok, na widoku uzyskamy poniższą zawartość.
Załóżmy, że jako wartość dla klucza courses chcemy wypisać wszystkie informacje z kursów.
Otrzymamy poniższy widok.
Nie są nam niezbędne wszystkie dane. Szczęśliwie możemy wybrać konkretne pola. Przykładowo:
identyfikator kursu,
nazwę kursu,
opis kursu.
Możemy także wyświetlić w słowniku kursu, pod kluczem topics informacje o wszystkich tematach.
Oczywiście, i je możemy zagnieździć, żądając jedynie określonych danych. Tak samo, w tematach, możemy wyświetlić opisane posty.
W przeglądarce wszystko działa należycie, ponieważ jesteśmy aktualnie zalogowani. Co jednak z interfejsem programistycznym? Na początek ustalamy w kontrolerze statycznym wymaganie, aby przy uruchamianiu metody feed, przez metodę require_token weryfikowany był token.
Możemy teraz spróbować wysłać zapytanie. Zostanie odrzucone z braku tokena.
curl -X GET http://localhost:3000/feed.json
Wyślijmy je więc z tokenem. Musimy w tym celu umieścić w zapytaniu nagłówek (-H) o kluczu Authentication i wartości Token token=WARTOŚĆ_TOKENA. Na początek wyślijmy w nim bzdurę.
curl -H "Authorization: Token token=bzdura" http://localhost:3000/feed.json
Zapytanie zostało ponownie odrzucone. Teraz wyślijmy poprawny token.
curl -H "Authorization: Token token=UZYSKANY_TOKEN" http://localhost:3000/feed.json
Wygląda na to, że mamy działającą autentykację. Przydatne będzie jeszcze dodanie deautentykacji. Uzupełnijmy więc metodę destroy kontrolera sesji, aby przy formacie json dewalidował token aktualnego studenta.
Spróbujmy wysłać zapytanie deautentykacji.
curl -X DELETE -H "Authorization: Token token=UZYSKANY_TOKEN" http://localhost:3000/logout.json
Token powinien zostać usunięty. Powtórna próba uzyskania zasobu /feed z tokenem nie powinna już zadziałać.
[info] Aktualny kod
Na koniec każdego modułu znajduje się łącze do pełnej wersji kodu, który powinien być jego wynikiem.
Last updated