Przede wszystkim, czym jest validacja i do czego jej potrzebujemy?
Validacją nazywamy szereg czynności polegający na sprawdzeniu zgodności z danymi schematami i wytycznymi, a także naszymi oczekiwaniami i przewidywaniami.
Programiści validację stosują najczęściej do sprawdzania poprawności danych wejściowych. Moim subiektywnym zdaniem sprawdzanie poprawności otrzymywanych danych jest absolutną koniecznością jeśli chcemy aby nasza aplikacja działała poprawnie.
Gdzie wykonywać validację?
Czynność tą powinniśmy wykonywać zarówno po stronie klienta jak i serwera. Podczas wypełniania chociażby formularza użytkownik powinien otrzymać informację zwrotną, jakie pola uzupełnił błędnie oraz co należy w nich poprawić. Głównym celem jest zwiększenie komfortu użytkownika podczas korzystania z naszej aplikacji. Nie chroni nas to jednak przed umyślnym wprowadzeniem niepoprawnych danych. Mechanizmy znajdujące się po stronie klienta można w banalny sposób obejść. Dlatego też sprawdzanie poprawności danych powinno przede wszystkim odbywać się na serwerze.
Praktyczny przykład validacji na serwerze
Kluczowym zagadnieniem tego artykułu jest omówienie paczki express-validator będącej rozszerzeniem frameworka Express.js. Jak sama nazwa wskazuje, rozszerzenie to umożliwia nam tworzenie validatorów. W tym wpisie będziemy sprawdzać poprawność danych podczas rejestracji nowego użytkownika. Na samym początku musimy zainstalować naszą paczkę:
npm install --save express-validator
Po zainstalowaniu pakietu możemy już importować interesujące nas komponenty. Przede wszystkim potrzebujemy skorzystać z funkcji check(). Funkcja check() jako parametr przyjmuje string z nazwą pola, które należy sprawdzić. Potem następuje spora dowolność w validacji. Tutaj możemy korzystać z istniejących w JS funkcji jak chociażby trim(), którą możemy obciąć nadmiar białych znaków. Innym przykładem może być matches(), gdzie możemy skorzystać z wyrażenia regularnego.
Oprócz tego możemy skorzystać z funkcji dostępnych dzięki naszej paczce. Jednymi z najbardziej przydatnych są: isEmpty(), która sprawdza, czy nie otrzymaliśmy pustego ciągu znaków. Możemy też skorzystać z negacji not(), łącząc ją z poprzednią funkcją. Otrzymujemy dzięki temu kombinację sprawdzającą, czy dany ciąg znaków nie jest pusty – niezmiernie przydatne w trakcie validacji. Podobnie do funkcji isEmpty() działają funkcje takie jak: isArray(), isEmail() i isBoolean(). Jeśli chcemy sprawdzać jakąś wartość, ale nie chcemy aby jej podanie było obowiązkowe możemy skorzystać z funkcji optional(). Samych funkcji oczywiście jest znacznie więcej. Link do dokumentacji znajdziesz w źródłach na końcu tego wpisu.
Zwieńczeniem validatora jest funkcja withMessage() zwracająca opracowany przez nas komunikat w przypadku, gdy nasze dane nie przejdą validacji. Należy pamiętać, aby komunikat zwrotny jednoznacznie określał na czym polega błąd, który popełnił użytkownik. Dlatego też w swoich przykładach sprawdzam niektóre pola kilkukrotnie, ponieważ potrzebuję sprawdzić więcej niż jedną rzecz. Każdy przypadek powinien posiadać osobny, komunikat. Zwróć uwagę na pola takie jak password czy email.
Zobaczmy jak może prezentować się prosty validator:
import { check } from 'express-validator/check';
exports.validateRegister = [
check('name').trim().not().isEmpty().withMessage('Name is required.'),
check('password').trim().not().isEmpty().withMessage('Password is required.'),
check('password').trim().isLength({ min: 8 }).withMessage('Password must be at least 8 characters long.'),
check('email').trim().not().isEmpty().withMessage('E-mail is required.'),
check('email').trim().isEmail().withMessage('Invalid e-mail address.'),
check('pictures').optional().isArray().withMessage('Invalid data format.')
];
Sanityzacja
Kolejnym krokiem będzie sanityzacja danych. Sama sanityzacja pozwala nam uchronić się chociażby przed atakami takimi jak na przykład XSS. Zainstalowana przez nas paczka posiada również mechanizmy pozwalające na sanityzację danych i idąc tropem XSS, zamianę znaków specjalnych jak na przykład nawiasy domknięte czy slesze na encje. Do tej czynności posłuży nam funkcja sanitizeBody(), która jako parametr przyjmuje string z nazwą pola. Alternatywnie możemy użyć gwiazdki, jeśli chcemy pracować z każdą przekazaną wartością. Do zamiany na encje posłuży nam funkcja escape().
import { check } from 'express-validator/check';
import { sanitizeBody } from 'express-validator/filter';
exports.validateRegister = [
check('name').trim().not().isEmpty().withMessage('Name is required.'),
check('password').trim().not().isEmpty().withMessage('Password is required.'),
check('password').trim().isLength({ min: 8 }).withMessage('Password must be at least 8 characters long.'),
check('email').trim().not().isEmpty().withMessage('E-mail is required.'),
check('email').trim().isEmail().withMessage('Invalid e-mail address.'),
check('pictures').optional().isArray().withMessage('Invalid data format.'),
sanitizeBody('*').escape()
];
Customowe funkcje validujące
Omawiana w tym wpisie bibliotek udostępnia również tworzenie niestandardowych filtrów. Służy do tego funkcja custom(). Wewnątrz tej funkcji możemy stworzyć własne funkcje validujące.
W moim projekcie wykorzystałem funkcję custom() do sprawdzania poprawności wpisywanej daty. Niestety sama biblioteka express-validator nie posiada domyślnie takiej możliwości. Do sprawdzenia poprawności daty użyłem biblioteki Moment.js, która jest dedykowana operacjom na dacie i czasie. Obecnie nasz kod wygląda tak:
import { check } from 'express-validator/check';
import { sanitizeBody } from 'express-validator/filter';
import moment from 'moment';
exports.validateRegister = [
check('name').trim().not().isEmpty().withMessage('Name is required.'),
check('password').trim().not().isEmpty().withMessage('Password is required.'),
check('password').trim().isLength({ min: 8 }).withMessage('Password must be at least 8 characters long.'),
check('email').trim().not().isEmpty().withMessage('E-mail is required.'),
check('email').trim().isEmail().withMessage('Invalid e-mail address.'),
check('birthDate').trim().not().isEmpty().withMessage('Date of birth is required.'),
check('birthDate').trim().custom(value => moment(value, 'YYYY-MM-DD').isValid()).withMessage('Invalid date of birth.'),
check('pictures').optional().isArray().withMessage('Invalid data format.'),
sanitizeBody('*').escape()
];
Implementacja
Mając już stworzone reguły należy ich użyć. Oprócz samych reguł potrzebujemy jeszcze funkcji, która powiadomi użytkownika jakie dane wprowadził niepoprawnie i co zależy w nich poprawić. Poniżej przedstawiam gotowe rozwiązanie, które możesz wykorzystać w swoim projekcie:
import {validationResult} from "express-validator/check";
exports.checkValidation = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(422).json({
success: false,
data: req.body,
errors: errors.mapped()
});
}
next();
};
Ostatnim krokiem będzie zaimplementowanie stworzonego przez nas kodu. W moim przypadku będzie to router, w którym znajduje się ścieżka do tworzenia nowego konta użytkownika:
import { Router } from "express";
import authController from "../controllers/authController";
import { validateRegister } from "../validators/usersValidator";
import { checkValidation } from "../validators/checkValidation";
const api = Router();
api.post('/register',
validateRegister,
checkValidation,
authController.register
);
module.exports = api;
Więcej przykładów
W tym wpisie nie przedstawiłem wszystkich możliwości express-validatora. Niemniej jednak uważam, że zestaw informacji, jaki przedstawiłem pozwolą na szybkie wsiąknięcie w temat validacji w Express.js.
Przedstawiony w tym wpisie kod jest częścią tworzonego przeze mnie projektu. Jeśli zaciekawił Cię temat validacji to zapraszam Cię do sprawdzenia mojego repozytorium na GitHub’ie. Jeśli widzisz w nim błędy lub chciałbyś coś w nim zmienić to zachęcam do tworzenia issues i pull requestów.
Jeśli coś jest niezrozumiałe, bądź też widzisz błąd, to daj mi o tym znać w komentarzu! Zachęcam też do zapoznania się ze źródłami i materiałami dodatkowymi, gdzie znajdziesz chociażby zupełnie inne podejście do walidacji, które również jest jak najbardziej poprawne (drugi link).
Źródła i materiały dodatkowe
https://github.com/elszczepano/FindMates-API
https://express-validator.github.io/docs
https://youtu.be/HsMxii2qOJE
a czy mogę użyć check(’text’).replace(’>’, ”), tak żeby mi sprawdził txt czy sa < i pozamieniał je?
Osobiście skorzystałbym raczej z
sanitizeBody('text').escape()
dzięki 🙂