Weryfikacja hasła podczas rejestracji użytkownika

Obecnie wiele stron internetowych posiada możliwość rejestracji. Istnieje kilka sposobów na obsługę użytkowników (np. w oparciu o API Facebook) ale my zajmiemy się problemem budowy samodzielnego formularza rejestracyjnego, a dokładniej etapem weryfikacji wprowadzanego hasła.

Generalnie istnieje kilka metod wyboru hasła. Najczęściej stosowanym rozwiązaniem jest stworzenie pola, w którym użytkownik podaje propozycję swojego hasła. Jeśli spełni ono wymagania narzucone przez programistę to hasło zostaje w formie zakodowanej zapisane do bazy danych. Drugim sposobem jest automatyczne wygenerowanie losowego hasła, które po pierwszym zalogowaniu użytkownik musi zmienić, ale i tutaj dochodzimy do etapu weryfikacji reguł wg których powinno być zbudowane hasło.

Krok 1 – ustalenie reguł walidujących

Obecnie coraz częściej zdarzają się próby włamań na konta użytkowników w związku z tym zaleca się tworzenie możliwe mocnych haseł. Nie daje to gwarancji pełnego bezpieczeństwa lecz z pewnością istotnie utrudnia złamanie hasła.

W naszym przykładzie założymy kilka reguł jakie musi spełnić hasło:

  • długość o 8 do 30 znaków
  • minimum jedna duża litera,
  • minimum jedna mała litera,
  • minimum jedna cyfra.

Zakładamy, że dopuszczone są wyłącznie litery alfabetu bez polskich znaków (dla uproszczenia przykładów – w praktyce łatwo można dodać ewentualnie również obsługę polskich znaków), cyfr i wybranych znaków specjalnych jak !,?,_,-. Nie dopuszczamy stosowania w haśle innych znaków, w tym spacji, tabulacji itp.

Metoda 1 – Wielokrotne wywołanie metody test

Gdy przeglądam fora internetowe, kody stron czy nawet książki to najczęściej rozwiązanie naszego problemu wyrażone jest w postaci wielokrotnego wywoływania metody test obiektu RegExp w sposób podobny do pokazanego poniżej:

function validate(pass) {
return typeof pass !== 'undefined' &&
pass.length >= 8 &&
pass.length <= 30 &&
/[A-Z]/.test(pass) &&
/[a-z]/.test(pass) &&
/\d/.test(pass);
}

validate('haslo111'); //false
validate('ABcd1'); //false
validate('Haslo111'); //true
validate('Haslo111abcd'); //true
validate(); //false

Na początku sprawdzamy, czy w ogóle metodzie przekazano jakiś parametr, a następnie testujemy, czy jego długość zawiera się między 8 a 30. Będąc bardzo dokładnym powinniśmy również sprawdzić, czy parametr funkcji jest zmienną typu string, jednakże pominiemy to w naszym przykładzie, gdyż w praktyce każda wartość pobierana z pól typu input jest wartością string, więc istnieje znikome ryzyko przekazania do funkcji złego typu wartości.

Następnie sprawdzamy, czy ciąg „pass” zawiera: jedną wielką literę, jedną małą literę i jedną cyfrę. Zwróć uwagę, że nie stosujemy tutaj flagi przeszukiwania globalnego „g” gdyż interesuje nas i tak jedynie pierwsze dopasowanie do danego wzorca (założyliśmy minimum jedną wielką literę, więc może ich być również więcej).

Jak widać mamy tutaj aż sześć reguł uruchamianych kolejno jedna po drugiej. Operator AND działa w taki sposób, że w przypadku wystąpienia pierwszego fałszu przestaje sprawdzać kolejne warunki. Bardzo szybko odrzucimy więc np. hasła zbyt krótkie lub zbyt długie.

Następnie jednak wywołamy od jednej do trzech metod testujących względem obiektu RegExp co jest już czynnością bardziej obciążającą komputer użytkownika.

Metoda 2 – Zróbmy to jednym wyrażeniem regularnym

Powyższa metoda działa poprawnie jednakże osobiście uważam, że lepszym rozwiązaniem jest stworzenie jednego kompleksowego wyrażenia regularnego, które zweryfikuje od razu wszystkie narzucone reguły.

Niektórzy w tym miejscu zarzucą mi, że przecież bardziej skomplikowany RegExp jest dłużej sprawdzany niż wyrażenia krótkie czy weryfikacja z użyciem operatora typeof oraz właściwości length.

Tak, zgadzam się, że stosując jedno długie wyrażenie pogarszam wydajność pracy aplikacji. W praktyce jednak mówimy tutaj o ułamkach sekund, natomiast w większości przypadków na etapie rejestracji użytkownika możemy sobie pozwolić na pewną stratę wydajności gdyż nie jest to krytyczny proces aplikacji (związany np. z wczytywaniem strony czy żądaniami Ajax).

Spróbujmy zatem powyższy przykład zamienić na jedno wyrażenie RegExp:

function validate(pass) {
var reg = /^(?=.*[A-Z])(?=.*[a-z])(?=.*\d).{8,30}$/;
return reg.test(pass);
}

validate('haslo111'); //false
validate('ABcd1'); //false
validate('Haslo111'); //true
validate('Haslo111abcd'); //true
vlidate(); //false

W wyrażeniu regularnym zastosowałem tzw. wyprzedzenia, czyli dopasowanie elementu, jeśli po nim znajduje się określony wzorzec. Dopasowanie wyprzedzające zawiera się w nawiasach okrągłych i poprzedza symbolem „?=”. Oznacza to, że element zostanie dopasowany tylko wtedy, gdy po nim znajduje się wyrażenie wyprzedzające.

W naszym przypadku elementem startowym, względem którego analizujemy dopasowanie wyprzedzeń jest początek wyrażenia (symbol „^”).

Zapis (?=.*[A-Z])  wyrażeniu wyprzedzającym oznacza, że musi wystąpić dowolny znak dowolną ilość razy (czyli również zero razy), a po nim jeden raz musi wystąpić wielka litera. Analogicznie sprawdzamy wystąpienie małej litery oraz cyfry.

Na koniec określamy, że w całym wyrażeniu musi znaleźć się ciąg dowolnych znaków (symbol kropki) w ilości od 8 do 30 (czyli wyrażeniem regularnym zastąpiliśmy testowanie właściwości length).

Według mnie druga metoda jest lepszym rozwiązaniem. Dla osób znających wyrażenia regularne obie metody są równie czytelne, a zyskujemy na długości kodu. Jak mówiłem wcześniej, zakładam, że kwestia wydajności na poziomie ułamków sekund nie ma w tym wypadku większego znaczenia.

Czy to na pewno koniec weryfikacji hasła?

Część czytelników na pewno zauważyła jeden poważny błąd zarówno w metodzie 1 jak i 2, związany z ostatnią regułą jaką wprowadziliśmy w pierwszym akapicie artykułu.

Napisałem tam, że pozwalamy, aby hasło składało się wyłącznie z liter alfabetu amerykańskiego (czyli bez polskich znaków), cyfr oraz znaków specjalnych jak wykrzyknik, znak zapytania, dolne podkreślenie oraz myślnik. Nie dopuszczamy żadnych innych znaków, w tym również białych znaków – spacje, tabulatory itp.

Spróbuj z użyciem obu wcześniejszych funkcji zweryfikować hasło „Haslo111++1”. W obu przypadkach otrzymamy wartość true i najprawdopodobniej hasło takie w wersji zakodowanej zostanie zapisane w bazie danych. Pewien problem może pojawić się już na etapie przesyłania danych na serwer lub przy ich obróbce np. w PHP (trzeba odpowiednio zabezpieczyć się przed niektórymi znakami dodając znaki ucieczki).

My dopuściliśmy stosowanie tylko czterech znaków specjalnych, które łatwo zapamiętać. Mało prawdopodobne jest, aby użytkownik zapamiętał wyżej proponowane hasło z użyciem „++”, tym bardziej, że praktycznie na większości stron i aplikacji nie będzie mógł go użyć.

Zmodyfikujmy więc nieco naszą pierwszą metodę:

function validate(pass) {
return /^[-\w!?]{8,30}$/.test(pass) &&
/[A-Z]/.test(pass) &&
/[a-z]/.test(pass) &&
/\d/.test(pass);
}

validate('Haslo111++1'); //false
validate('Haslo111'); //true

Usunęliśmy jednocześnie sprawdzanie właściwości length i zastąpiliśmy to wyrażeniem regularnym, które sprawdza, czy w ciągu znajdują się wyłącznie znaki z dopuszczonego zakresu (zwróć uwagę na granice ciągu „^” oraz „$”).

A teraz podobna zmiana w drugiej metodzie:

function validate(pass) {
var reg = /^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)[-\w!?]{8,30}$/;
return reg.test(pass);
}

validate('Haslo111++1'); //false
validate('Haslo111'); //true

Teraz funkcje poprawnie weryfikują hasła według założonych przez nas reguł. Pamiętaj, że zastosowane oznaczenie „\w” zawiera w sobie „[A-Za-z0-9_]” (nie zapomnij o dolnym podkreśleniu!). Znaki „!” oraz „?” użyte wewnątrz nawiasów kwadratowych nie mają żadnego szczególnego znaczenia i są interpretowane dosłownie. Jeśli nie masz pewności co do sposobu interpretacji znaków przez silnik RegExp to użyj przed nimi znak ucieczki „\!”. Zalecam jednak ostrożność i nie stosowanie wewnątrz zakresów (czyli nawiasów kwadratowych) zapisów „?!” gdyż zestawienie tych znaków w nawiasie okrągłym oznacza wyprzedzenie negatywne. Osobiście kolejność „?!” pozostawiam wyłącznie dla definiowania wyprzedzeń negatywnych, a w innych wypadkach celowo stosuję odwróconą kolejność co uważam, że ułatwia późniejsze analizowanie wyrażenia, szczególnie gdy jest ono bardziej skomplikowane.

Dodatkowo jeśli w zakresie znaków (w nawiasach kwadratowych) chcesz ująć również myślnik, to zalecam wskazanie go jako pierwszy lub ostatni znak w nawiasach „[ … ]”. Pozwoli to uniknąć problemu związanego z przypadkowym wskazaniem zakresu znaków od … do zamiast dosłownie symbolu „-„. Ewentualnie jeśli znajdzie się on wewnątrz ciągu w nawiasach to zastosuj znak ucieczki.

 

W artykule w początkowych przykładach celowo pominąłem problem określonego zakresu znaków aby wyraźnie zwrócić uwagę czytelnika na konieczność wykonania bardzo dokładnych testów – szczególnie przy tak ważnych kwestiach jak hasła, nazwy użytkownika itp.

Ponad to nigdy nie zapominaj, że weryfikacja po stronie klienta w JavaScript to tylko dodatek, umożliwiający np. eleganckie wyświetlenie informacji o błędnym proponowanym przez użytkownika haśle bez konieczności nawiązywania połączenia z serwerem. Ostateczną weryfikację zawsze przeprowadzaj po stronie serwera, gdzie użytkownik nie ma dostępu (nie mówimy tutaj o włamaniach hakerskich… nie popadajmy w totalną panikę – chyba, że projektujesz stronę banku… ale wtedy raczej nie szukasz informacji na blogu poświęconym podstawom JavaScript 🙂