Identyfikacja użytkownika jest niezbędnym elementem prawie każdej aplikacji dostępnej zdalnie. Najczęściej identyfikacji dokonujemy poprzez weryfikację dostarczonej przez użytkownika kombinacji login / hasło. Po uwierzytelnieniu umożliwiamy użytkownikowi korzystanie z całości lub części funkcjonalności aplikacji. Jak w takim razie najlepiej zrealizować proces identyfikacji i uwierzytelnienia w GWT?
Uwierzytelnianie a aplikacja GWT
Przypomnijmy sobie najpierw jak przebiega typowy proces uwierzytelnienia i jakie role w nim występują. Po pierwsze, mamy użytkownika, który wypełnia pola interfejsu aplikacji, przeznaczone na wprowadzenie loginu i hasła. Po drugie, mamy procedurę weryfikacji tych danych, najprawdopodobniej odpytującą warstwę trwałości – a więc pewną bazę – w celu skonfrontowania wartości dostarczonych przez użytkownika z tymi, przyjmowanymi za prawidłowe. Hasła użytkowników powinny być przechowywane w warstwie trwałości w postaci zaszyfrowanej, aby nie ujawnić prawdziwego hasła nawet w przypadku wycieku danych z bazy. W tym celu możemy skorzystać z jednej z bibliotek Java do szyfrowania (np. jBCrypt) lub szyfrowania realizowanego przez bazę danych.
Specyfiką aplikacji napisanej w GWT jest działanie interfejsu całkowicie po stronie klienta, pod kontrolą przeglądarki internetowej. Informacje podane przez użytkownika muszą zatem być przekazane przez sieć, do zdalnego serwera, gdzie nastąpi weryfikacja. Czy proces uwierzytelnienia mógłby być przeprowadzony w aplikacji klienckiej? Oczywiście nie, bo kod działający lokalnie w przeglądarce użytkownika można łatwo zmodyfikować i obejść jakiekolwiek zabezpieczenia, pomijając sprawdzenie poprawności danych.
Pierwsze pytanie jakie rodzi nam się w głowie, to w jaki sposób zabezpieczyć dane przesyłane przez sieć. Wysyłamy kombinację login i hasło, czy nie powinny być one w jakiś sposób zaszyfrowane? Nie jest to niestety proste, bo jak pamiętamy, nasza aplikacja po kompilacji staje się kodem JavaScriptowym, działającym w przeglądarce internetowej. Działamy więc w dużo bardziej ograniczonym środowisku – ze względu na język i platformę uruchomieniową – niż, chociażby aplikacje standalone w Javie. W szczególności, mamy zawężone możliwości wykonywania bardziej skomplikowanych operacji matematycznych, a co za tym idzie większość bibliotek kryptograficznych jest bezużyteczna w aplikacjach GWT. Poza tym należy pamiętać, że zaszyfrowanie hasła po stronie klienta i przesłanie go otwartym kanałem niewiele nam daje. Po pierwsze, kod kliencki jest całkowicie modyfikowalny, więc algorytm szyfrowania można zmienić, usunąć lub zasymulować we własnej aplikacji atakującego. Po drugie, podsłuchanie zaszyfrowanego hasła jest tak samo wartościowe dla atakującego, co podsłuchanie hasła niezaszyfrowanego – w pierwszym przypadku serwer weryfikuje przecież poprawność na podstawie wartości zaszyfrowanej.
Cóż nam pozostaje? Możemy pozostać przy przesyłaniu hasła otwartym tekstem do serwera (pamiętając jednak o szyfrowaniu w bazie), możemy wykorzystać komunikację SSL, ażeby uniemożliwić podsłuch lub – w końcu – możemy całkowicie zrezygnować z uwierzytelniania w aplikacji GWT i przeprowadzić ją bezpośrednio na serwerze. Aplikacja GWT zostanie załadowana dopiero po uwierzytelnieniu i przyjmować będzie, że zawsze działa w kontekście pewnego uwierzytelnionego użytkownika.
Formularz logowania a automatyczne zapisywanie hasła
W tym artykule nie będę się zajmował szczegółowo kwestiami bezpieczeństwa, chciałbym jednak zwrócić uwagę na mechanizm, który od pewnego czasu bardzo ułatwia nam życie jako użytkownikom systemów. Mianowicie każda chyba z istniejących obecnie przeglądarek umożliwia zapisywanie hasła po pierwszym wpisaniu ich w odpowiednie pola aplikacji internetowej i automatyczne wypełnianie przy każdym kolejnym włączeniu owej aplikacji. Skąd przeglądarka wie, że pola tekstowe, które widnieją na stronie służą do “zalogowania się” użytkownika? Reguły nieco różnią się pomiędzy przeglądarkami, ale generalnie na stronie musi znajdować się pole tekstowe i pole typu password w formularzu z przyciskiem “submit”, a formularz ten nie może być generowany dynamicznie przez JavaScript (musi istnieć w oryginalnym HTMLu). Pamiętając, że aplikacja GWT tworzy swój interfejs po stronie klienta dynamicznie, poprzez modyfikację dokumentu za pomocą JavaScriptu, widzimy od razu, że w przypadku formularza zrealizowanego w GWT automatyczne zapisywanie hasła nie zadziała.
Co możemy zatem zrobić? Chciałbym przedstawić trzy rozwiązania, które różnią się rezultatem, a wybór jednego z nich powinien nastąpić po zastanowieniu się, czy najbardziej zależy nam na kompatybilności ze wszystkimi przeglądarkami, czy na łatwości oprogramowania procesu uwierzytelnienia, czy też na możliwości utworzenia formularza z kodu GWT.
Podejście pierwsze
W skrócie pomysł można opisać następująco: na stronie, będącej kontenerem naszej aplikacji tworzymy ukryty formularz do wpisywania loginu i hasła przez użytkowników (w statycznym HTMLu), natomiast w kodzie GWT pokazujemy ów formularz wtedy, kiedy jest potrzebny i niskopoziomowymi instrukcjami, umożliwiającymi dostęp do elementów HTML pobieramy dane z pól tekstowych i przesyłamy na serwer. Po odebraniu i pozytywnym zinterpretowaniu odpowiedzi ukrywamy formularz i umożliwiamy dalsze działanie aplikacji. Podejście takie zaproponowane zostało w oficjalnym FAQu GWT i można zaimplementować je w następujący sposób (MyApp.html):
W kodzie aplikacji, z formularza skorzystalibyśmy następująco (opakowujemy formularz HTML w komponent GWT i sygnalizujemy, że chcemy, żeby akcja formularza była wykonana w iframie zagnieżdżonym na stronie – drugi argument metody FormPanel.wrap jest ustawiony na true):
Element formElement = Document.get().getElementById("loginForm");
if (formElement != null) {
Element formDiv = Document.get().getElementById("hiddenDiv");
if (formDiv != null) {
formDiv.setAttribute("style", "visibility:visible");
}
FormPanel form = FormPanel.wrap(formElement, true);
form.addSubmitCompleteHandler(new SubmitCompleteHandler() {
@Override
public void onSubmitComplete(SubmitCompleteEvent event) {
zinterpretuj(event.getResults());
}
});
}
W tym podejściu na serwerze musimy wygenerować odpowiedź w postaci tekstowej (Content-Type: text/html) – np. JSON i zinterpretować po stronie klienta, w celu podjęcia decyzji, czy dopuścić użytkownika do dalszej części aplikacji. Można do formularza również dodać słuchacza SubmitHandler, w którym zaimplementujemy wstępną walidację wprowadzonych wartości przed wysłaniem na serwer.
Drugie podejście
W tym podejściu chcemy wykorzystać standardową dla GWT komunikację z serwerem na zasadzie zdalnego wywoływania metod, a nie samodzielnego generowania odpowiedzi na serwerze w postaci tekstowej. Rozwiązanie, zaproponowane w tym wątku, opiera się pomyśle, aby akcją wykonywaną przy wysłaniu formularza nie było fizyczne przesłanie danych na serwer, a wywołanie pośredniej metody JavaScriptowej, która dopiero zawoła zdalną metodę, tak jak zwykle robi się to w GWT. Nasz plik .html wygląda analogicznie jak poprzednio, z różnicą dotyczącą domyślnej akcji po wysłaniu formularza:
Natomiast w kodzie wykonujemy następujące operacje (tym razem formularz nie musi znajdować się w zagnieżdżonym iframie):
injectLoginFunction(this);
Element formElement = Document.get().getElementById("loginForm");
if (formElement != null) {
Element formDiv = Document.get().getElementById("hiddenDiv");
if (formDiv != null) {
formDiv.setAttribute("style", "visibility:visible");
}
FormPanel form = FormPanel.wrap(formElement, false);
}
Metoda injectLoginFunction dodaje do strony funkcję JavaScriptową, która pośredniczy w wywołaniu RPC:
private native void injectLoginFunction(LoginViewImpl inst) /*-{
$wnd.__gwt_login = function() {
inst.@pl.com.sages.client.view.LoginViewImpl::doLogin()();
}
}-*/;
public void doLogin() {
loginValue = InputElement.as(Document.get()
.getElementById("login")).getValue();
passwordValue = InputElement.as(Document.get()
.getElementById("password")).getValue();
rpcService.verifyUser(login, password, new AsyncCallback< ...
// zdalne wywołanie metody na serwerze i rejestracja słuchacza,
// interpretującego odpowiedź
}
To podejście nie działa niestety w przeglądarkach opartych na WebKicie, np. w Chrome.
Trzecie podejście
Ten sposób rozwiązania formularza do identyfikacji użytkownika został zaproponowany na blogu Matta Raible. Założenie było takie, żeby wykorzystać komponenty GWT jakie pola do wpisywania loginu i hasła (również z zewnętrznych bibliotek – np. GXT), a nie poprzestawać na standardowych kontrolkach HTML. W tym wypadku akcją przypisaną do ukrytego formularza HTML musi być prawidłowy URL, np. /myApp/login, inaczej (przynajmniej w moich testach) zapamiętywania hasła nie działa. W naszym kodzie przepisujemy wartości podane przez użytkownika (wprowadzone do naszych komponentów GWT) do owego ukrytego formularza:
Document.get().getElementById("login").setAttribute("value",
loginField.getValue());
Document.get().getElementById("password").setAttribute("value",
passwordField.getValue());
Element formElement = Document.get().getElementById("loginForm");
FormPanel form = FormPanel.wrap(formElement, true);
form.submit();
Wysłanie formularza powoduje pojawienie się pytania o zgodę na zapisania hasła w przeglądarce. Pozostaje jeszcze kwestia przepisania wartości pól z automatycznie wypełnionego przez przeglądarkę formularza HTML na nasze komponenty GWT. Matt Raible proponuje wykorzystanie klasy Timer, żeby opóźnić przepisanie wartości pól, aż będą one dostępne dla skryptu, ale w moich testach nie było to potrzebne:
Zapisywanie hasła użytkownika w przeglądarce w aplikacjach GWT
Łukasz Kobyliński
Identyfikacja użytkownika jest niezbędnym elementem prawie każdej aplikacji dostępnej zdalnie. Najczęściej identyfikacji dokonujemy poprzez weryfikację dostarczonej przez użytkownika kombinacji login / hasło. Po uwierzytelnieniu umożliwiamy użytkownikowi korzystanie z całości lub części funkcjonalności aplikacji. Jak w takim razie najlepiej zrealizować proces identyfikacji i uwierzytelnienia w GWT?
Uwierzytelnianie a aplikacja GWT
Przypomnijmy sobie najpierw jak przebiega typowy proces uwierzytelnienia i jakie role w nim występują. Po pierwsze, mamy użytkownika, który wypełnia pola interfejsu aplikacji, przeznaczone na wprowadzenie loginu i hasła. Po drugie, mamy procedurę weryfikacji tych danych, najprawdopodobniej odpytującą warstwę trwałości – a więc pewną bazę – w celu skonfrontowania wartości dostarczonych przez użytkownika z tymi, przyjmowanymi za prawidłowe. Hasła użytkowników powinny być przechowywane w warstwie trwałości w postaci zaszyfrowanej, aby nie ujawnić prawdziwego hasła nawet w przypadku wycieku danych z bazy. W tym celu możemy skorzystać z jednej z bibliotek Java do szyfrowania (np. jBCrypt) lub szyfrowania realizowanego przez bazę danych.
Specyfiką aplikacji napisanej w GWT jest działanie interfejsu całkowicie po stronie klienta, pod kontrolą przeglądarki internetowej. Informacje podane przez użytkownika muszą zatem być przekazane przez sieć, do zdalnego serwera, gdzie nastąpi weryfikacja. Czy proces uwierzytelnienia mógłby być przeprowadzony w aplikacji klienckiej? Oczywiście nie, bo kod działający lokalnie w przeglądarce użytkownika można łatwo zmodyfikować i obejść jakiekolwiek zabezpieczenia, pomijając sprawdzenie poprawności danych.
Pierwsze pytanie jakie rodzi nam się w głowie, to w jaki sposób zabezpieczyć dane przesyłane przez sieć. Wysyłamy kombinację login i hasło, czy nie powinny być one w jakiś sposób zaszyfrowane? Nie jest to niestety proste, bo jak pamiętamy, nasza aplikacja po kompilacji staje się kodem JavaScriptowym, działającym w przeglądarce internetowej. Działamy więc w dużo bardziej ograniczonym środowisku – ze względu na język i platformę uruchomieniową – niż, chociażby aplikacje standalone w Javie. W szczególności, mamy zawężone możliwości wykonywania bardziej skomplikowanych operacji matematycznych, a co za tym idzie większość bibliotek kryptograficznych jest bezużyteczna w aplikacjach GWT. Poza tym należy pamiętać, że zaszyfrowanie hasła po stronie klienta i przesłanie go otwartym kanałem niewiele nam daje. Po pierwsze, kod kliencki jest całkowicie modyfikowalny, więc algorytm szyfrowania można zmienić, usunąć lub zasymulować we własnej aplikacji atakującego. Po drugie, podsłuchanie zaszyfrowanego hasła jest tak samo wartościowe dla atakującego, co podsłuchanie hasła niezaszyfrowanego – w pierwszym przypadku serwer weryfikuje przecież poprawność na podstawie wartości zaszyfrowanej.
Cóż nam pozostaje? Możemy pozostać przy przesyłaniu hasła otwartym tekstem do serwera (pamiętając jednak o szyfrowaniu w bazie), możemy wykorzystać komunikację SSL, ażeby uniemożliwić podsłuch lub – w końcu – możemy całkowicie zrezygnować z uwierzytelniania w aplikacji GWT i przeprowadzić ją bezpośrednio na serwerze. Aplikacja GWT zostanie załadowana dopiero po uwierzytelnieniu i przyjmować będzie, że zawsze działa w kontekście pewnego uwierzytelnionego użytkownika.
Formularz logowania a automatyczne zapisywanie hasła
W tym artykule nie będę się zajmował szczegółowo kwestiami bezpieczeństwa, chciałbym jednak zwrócić uwagę na mechanizm, który od pewnego czasu bardzo ułatwia nam życie jako użytkownikom systemów. Mianowicie każda chyba z istniejących obecnie przeglądarek umożliwia zapisywanie hasła po pierwszym wpisaniu ich w odpowiednie pola aplikacji internetowej i automatyczne wypełnianie przy każdym kolejnym włączeniu owej aplikacji. Skąd przeglądarka wie, że pola tekstowe, które widnieją na stronie służą do “zalogowania się” użytkownika? Reguły nieco różnią się pomiędzy przeglądarkami, ale generalnie na stronie musi znajdować się pole tekstowe i pole typu password w formularzu z przyciskiem “submit”, a formularz ten nie może być generowany dynamicznie przez JavaScript (musi istnieć w oryginalnym HTMLu). Pamiętając, że aplikacja GWT tworzy swój interfejs po stronie klienta dynamicznie, poprzez modyfikację dokumentu za pomocą JavaScriptu, widzimy od razu, że w przypadku formularza zrealizowanego w GWT automatyczne zapisywanie hasła nie zadziała.
Co możemy zatem zrobić? Chciałbym przedstawić trzy rozwiązania, które różnią się rezultatem, a wybór jednego z nich powinien nastąpić po zastanowieniu się, czy najbardziej zależy nam na kompatybilności ze wszystkimi przeglądarkami, czy na łatwości oprogramowania procesu uwierzytelnienia, czy też na możliwości utworzenia formularza z kodu GWT.
Podejście pierwsze
W skrócie pomysł można opisać następująco: na stronie, będącej kontenerem naszej aplikacji tworzymy ukryty formularz do wpisywania loginu i hasła przez użytkowników (w statycznym HTMLu), natomiast w kodzie GWT pokazujemy ów formularz wtedy, kiedy jest potrzebny i niskopoziomowymi instrukcjami, umożliwiającymi dostęp do elementów HTML pobieramy dane z pól tekstowych i przesyłamy na serwer. Po odebraniu i pozytywnym zinterpretowaniu odpowiedzi ukrywamy formularz i umożliwiamy dalsze działanie aplikacji. Podejście takie zaproponowane zostało w oficjalnym FAQu GWT i można zaimplementować je w następujący sposób (MyApp.html):
<div id="hiddenDiv" style="visibility:hidden"> <form id="loginForm" action="/myApp/login"> <h1>Proszę się zalogować</h1> <center> <table> <tr><td>Login:</td> <td><input type="text" id="login" /></td></tr> <tr><td>Hasło:</td> <td><input type="password" id="password" /></td></tr> <tr><td><input type="submit" id="submit" /></td> <td><input type="reset" /></td></tr> </table></center> </form> </div>W kodzie aplikacji, z formularza skorzystalibyśmy następująco (opakowujemy formularz HTML w komponent GWT i sygnalizujemy, że chcemy, żeby akcja formularza była wykonana w iframie zagnieżdżonym na stronie – drugi argument metody FormPanel.wrap jest ustawiony na true):
Element formElement = Document.get().getElementById("loginForm"); if (formElement != null) { Element formDiv = Document.get().getElementById("hiddenDiv"); if (formDiv != null) { formDiv.setAttribute("style", "visibility:visible"); } FormPanel form = FormPanel.wrap(formElement, true); form.addSubmitCompleteHandler(new SubmitCompleteHandler() { @Override public void onSubmitComplete(SubmitCompleteEvent event) { zinterpretuj(event.getResults()); } }); }W tym podejściu na serwerze musimy wygenerować odpowiedź w postaci tekstowej (Content-Type: text/html) – np. JSON i zinterpretować po stronie klienta, w celu podjęcia decyzji, czy dopuścić użytkownika do dalszej części aplikacji. Można do formularza również dodać słuchacza SubmitHandler, w którym zaimplementujemy wstępną walidację wprowadzonych wartości przed wysłaniem na serwer.
Drugie podejście
W tym podejściu chcemy wykorzystać standardową dla GWT komunikację z serwerem na zasadzie zdalnego wywoływania metod, a nie samodzielnego generowania odpowiedzi na serwerze w postaci tekstowej. Rozwiązanie, zaproponowane w tym wątku, opiera się pomyśle, aby akcją wykonywaną przy wysłaniu formularza nie było fizyczne przesłanie danych na serwer, a wywołanie pośredniej metody JavaScriptowej, która dopiero zawoła zdalną metodę, tak jak zwykle robi się to w GWT. Nasz plik .html wygląda analogicznie jak poprzednio, z różnicą dotyczącą domyślnej akcji po wysłaniu formularza:
Natomiast w kodzie wykonujemy następujące operacje (tym razem formularz nie musi znajdować się w zagnieżdżonym iframie):
injectLoginFunction(this); Element formElement = Document.get().getElementById("loginForm"); if (formElement != null) { Element formDiv = Document.get().getElementById("hiddenDiv"); if (formDiv != null) { formDiv.setAttribute("style", "visibility:visible"); } FormPanel form = FormPanel.wrap(formElement, false); }Metoda injectLoginFunction dodaje do strony funkcję JavaScriptową, która pośredniczy w wywołaniu RPC:
private native void injectLoginFunction(LoginViewImpl inst) /*-{ $wnd.__gwt_login = function() { inst.@pl.com.sages.client.view.LoginViewImpl::doLogin()(); } }-*/; public void doLogin() { loginValue = InputElement.as(Document.get() .getElementById("login")).getValue(); passwordValue = InputElement.as(Document.get() .getElementById("password")).getValue(); rpcService.verifyUser(login, password, new AsyncCallback< ... // zdalne wywołanie metody na serwerze i rejestracja słuchacza, // interpretującego odpowiedź }To podejście nie działa niestety w przeglądarkach opartych na WebKicie, np. w Chrome.
Trzecie podejście
Ten sposób rozwiązania formularza do identyfikacji użytkownika został zaproponowany na blogu Matta Raible. Założenie było takie, żeby wykorzystać komponenty GWT jakie pola do wpisywania loginu i hasła (również z zewnętrznych bibliotek – np. GXT), a nie poprzestawać na standardowych kontrolkach HTML. W tym wypadku akcją przypisaną do ukrytego formularza HTML musi być prawidłowy URL, np. /myApp/login, inaczej (przynajmniej w moich testach) zapamiętywania hasła nie działa. W naszym kodzie przepisujemy wartości podane przez użytkownika (wprowadzone do naszych komponentów GWT) do owego ukrytego formularza:
Document.get().getElementById("login").setAttribute("value", loginField.getValue()); Document.get().getElementById("password").setAttribute("value", passwordField.getValue()); Element formElement = Document.get().getElementById("loginForm"); FormPanel form = FormPanel.wrap(formElement, true); form.submit();Wysłanie formularza powoduje pojawienie się pytania o zgodę na zapisania hasła w przeglądarce. Pozostaje jeszcze kwestia przepisania wartości pól z automatycznie wypełnionego przez przeglądarkę formularza HTML na nasze komponenty GWT. Matt Raible proponuje wykorzystanie klasy Timer, żeby opóźnić przepisanie wartości pól, aż będą one dostępne dla skryptu, ale w moich testach nie było to potrzebne:
loginField.setValue(((InputElement)Document.get() .getElementById("login")).getValue()); passwordField.setValue(((InputElement)Document.get() .getElementById("password")).getValue());Również i to podejście nie działa niestety w Chrome.