Zapis danych użytkowników w MongoDB
O MongoDB pisałam już w dwóch wcześniejszych postach: Jak zacząć z MongoDB? i MongoDB + Spring Data.
Dzisiaj postanowiłam zaimplementować wreszcie zapis danych z formularza użytkownika w mojej nierelacyjnej (dokumentowej) bazie danych.
W poprzednim tekście pokazałam, jakich adnotacji należy użyć w klasie User
, żeby jej instancje zostały poprawnie zapisane w bazie danych. Żeby faktycznie zapisać dane z formularza, musiałam tylko dopisać kilka linii w kodzie kontrolera:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
... @Autowired private UserRepository userRepository; ... @RequestMapping(value = "/signup", method = RequestMethod.POST) public String checkPersonInfo(@Valid User user, BindingResult bindingResult, Model model) { if (bindingResult.hasErrors() || !processPasswords(user, bindingResult)) { return "userform"; } model.addAttribute("currentUser", user); userRepository.save(user); return "userpage"; } |
Co zrobić z hasłami?
Zależało mi na spełnieniu trzech warunków:
- Użytkownik podaje hasło dwa razy, żeby zmniejszyć ryzyko literówki.
- Wpisywane hasło jest niewidoczne.
- Hasło w bazie danych nie jest przechowywane w postaci jawnej.
Dwa hasła
Dodałam następujące pola do klasy reprezentującej użytkownika:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
@Document public class User { ... @Transient @NotNull @Size(min=5,max=30) private String password1; @Transient @NotNull @Size(min=5,max=30) private String password2; ... } |
Specjalnie zaznaczyłam adnotację @Transient
. Dzięki jej użyciu wskazane pola (w tym wypadku hasła zapisane jawnym tekstem) nie zostaną zapisane w bazie danych.
Ukrycie literek
Pola musiałam również dopisać do formularza. Użyłam pola input
w wersji password
, żeby zamiast liter wyświetlić gwiazdki (czy tam kropki).
1 2 3 4 5 6 7 8 9 10 |
<tr> <td>Password:*</td> <td><input type="password" th:field="*{password1}" /></td> <td th:if="${#fields.hasErrors('password1')}" th:errors="*{password1}">Password error</td> </tr> <tr> <td>Repeat Password:*</td> <td><input type="password" th:field="*{password2}" /></td> <td th:if="${#fields.hasErrors('password2')}" th:errors="*{password2}">Password error</td> </tr> |
Funkcja hashująca
Wreszcie, hasła. Nie powinny być przechowywane w bazie danych w postaci jawnej. Powodów jest wiele. Na przykład taki: ludzie często używają tych samych haseł w wielu miejscach. Gdyby administrator bazy danych miał wgląd w hasła do aplikacji, mógłby podjąć próbę zalogowania się na konta użytkowników w innych serwisach (np. gmail) przy użyciu tego samego hasła i adresu email.
Oprócz pokazanych już dwóch pól do wprowadzenia hasła roboczego, klasa User
przechowuje jeszcze następujące dwie wartości:
1 2 3 4 5 6 7 8 9 10 11 |
@Document public class User { ... private String passwordEncrypted; private String passwordSalt; ... } |
passwordSalt
to losowa wartość doklejana do hasła przed jego “zaszyfrowaniem” (dzięki niej w bazie danych nie będzie widać, że dwa użytkownicy przypadkowo użyli tego samego hasła), passwordEncrypted
to wynik szyfrowania.
Losowanie i szyfrowanie realizuje następujący pomocniczy serwis, wywoływany z kontrolera dzięki adnotacji @Autowired
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
@Service public class PasswordsHelper { private SecureRandom generator; private String encryptionAlgorithm = "PBEWithMD5AndDES"; public PasswordsHelper() throws NoSuchAlgorithmException{ generator = new SecureRandom(); } public String getNextPasswordSeed(){ return String.valueOf(generator.nextInt()); } public String encrypt(String password){ SecretKey secretKey = new SecretKeySpec(password.getBytes(), encryptionAlgorithm); return secretKey.getEncoded().toString(); } ... } |
Kontroler wygląda tak:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
... @Controller public class UserController extends WebMvcConfigurerAdapter { ... @Autowired private PasswordsHelper passwordsHelper; @RequestMapping(value = "/signup", method = RequestMethod.POST) public String checkPersonInfo(@Valid User user, BindingResult bindingResult, Model model) { if (bindingResult.hasErrors() || !processPasswords(user, bindingResult)) { return "userform"; } ... return "userpage"; } private boolean processPasswords(User user, BindingResult bindingResult) { if (!user.getPassword1().equals(user.getPassword2())) { bindingResult.rejectValue("password1", "Passwords don't match", "Passwords don't match"); bindingResult.rejectValue("password2", "Passwords don't match", "Passwords don't match"); return false; } user.setPasswordSalt(passwordsHelper.getNextPasswordSeed()); user.setPasswordEncrypted(passwordsHelper.encrypt(user.getPasswordSalt() + user.getPassword1())); return true; } } |
Błąd 1: strona błędu zamiast komunikatu o błędzie w formularzu
Podanie niepoprawnych danych w formularzu z nieznanego mi powodu zaczęło przenosić mnie na moją własną stronę błędu (zamiast wyświetlenia komunikatu przy polu formularza, które spowodowało problem).
Co się okazało?
W pewnym momencie zmieniłam sygnaturę metody kontrolera z:
1 |
public String checkPersonInfo(@Valid User user, BindingResult bindingResult) { |
na:
1 |
public String checkPersonInfo(@Valid User user, Model model, BindingResult bindingResult) { |
ponieważ chciałam dopisać informacje do modelu.
Okazuje się, że parametr reprezentujący BindingResult
musi następować bezpośrednio po parametrze, przez który przekazywany jest dotyczący go obiekt (co ma sens, gdy jeden formularz obsługuje wiele obiektów).
Formularz zaczął działać poprawnie po zamianie kolejności parametrów:
1 |
public String checkPersonInfo(@Valid User user, BindingResult bindingResult, Model model) { |
Błąd 2: Error during execution of processor ‘org.thymeleaf.spring4.processor.attr.SpringErrorsAttrProcessor’
Kolejny błąd zaskoczył mnie, kiedy testowałam sytuację, w której użytkownik wpisze dwa różne hasła. Okazało się, że poczyniłam złe założenia na temat parametrów metody rejectValue, za pomocą której chciałam przekazać do formularza informację o wykryciu problemu. Napisałam:
1 |
bindingResult.rejectValue("password1", "Passwords don't match"); |
zakładając, że drugi parametr to komunikat o błędzie. Tymczasem – to kod błędu. Komunikat można przekazać dopiero w trzecim parametrze (poprawny kod widać na listingu z metodą processPasswords
). Błąd wynikał zatem z braku komunikatu o błędzie, który był wymagany w szablonie.
Wreszcie działa
Kod jest dostępny w moim repozytorium GitHub.

