Статьи

Формы, загрузка файлов и безопасность с Node.js и Express

Если вы создаете веб-приложение, вы, вероятно, столкнетесь с необходимостью создания HTML-форм в первый же день. Они — большая часть веб-опыта, и они могут быть сложными.

Обычно процесс обработки формы включает в себя:

  • отображение пустой HTML-формы в ответ на начальный GET
  • пользователь отправляет форму с данными в запросе POST
  • проверка как на клиенте, так и на сервере
  • повторное отображение формы, заполненной экранированными данными и сообщениями об ошибках, если недействительно
  • делать что-то с очищенными данными на сервере, если все это действительно
  • перенаправление пользователя или отображение сообщения об успешном завершении после обработки данных.

Обработка данных формы также связана с дополнительными соображениями безопасности.

Мы рассмотрим все это и объясним, как их создавать с помощью Node.js и Express — самой популярной веб-среды для Node. Сначала мы создадим простую контактную форму, где люди смогут безопасно отправлять сообщения и адреса электронной почты, а затем рассмотрим, что происходит при обработке загрузки файлов.

Контактная форма с электронной почтой и сообщением с ошибками проверки

Настроить

Убедитесь, что у вас установлена ​​последняя версия Node.js; node -v должен вернуть 8.9.0 или выше.

Загрузите начальный код отсюда с помощью git:

 git clone https://github.com/sitepoint-editors/node-forms.git cd node-forms npm install npm start 

Там не так уж много кода. Это просто базовая экспресс-установка с шаблонами EJS и обработчиками ошибок:

 // server.js const path = require('path') const express = require('express') const layout = require('express-layout') const routes = require('./routes') const app = express() app.set('views', path.join(__dirname, 'views')) app.set('view engine', 'ejs') const middleware = [ layout(), express.static(path.join(__dirname, 'public')), ] app.use(middleware) app.use('/', routes) app.use((req, res, next) => { res.status(404).send("Sorry can't find that!") }) app.use((err, req, res, next) => { console.error(err.stack) res.status(500).send('Something broke!') }) app.listen(3000, () => { console.log(`App running at http://localhost:3000`) }) 

Корневой URL / просто отображает представление index.ejs .

 // routes.js const express = require('express') const router = express.Router() router.get('/', (req, res) => { res.render('index') }) module.exports = router 

Отображение формы

Когда люди делают запрос GET для /contact , мы хотим отобразить новое представление contact.ejs :

 // routes.js router.get('/contact', (req, res) => { res.render('contact') }) 

Форма обратной связи позволит им отправить нам сообщение и свой адрес электронной почты:

 <!-- views/contact.ejs --> <div class="form-header"> <h2>Send us a message</h2> </div> <form method="post" action="/contact" novalidate> <div class="form-field"> <label for="message">Message</label> <textarea class="input" id="message" name="message" rows="4" autofocus></textarea> </div> <div class="form-field"> <label for="email">Email</label> <input class="input" id="email" name="email" type="email" value="" /> </div> <div class="form-actions"> <button class="btn" type="submit">Send</button> </div> </form> 

Посмотрите, как это выглядит на http://localhost:3000/contact .

Отправка формы

Чтобы получить значения POST в Express, сначала необходимо включить промежуточное программное обеспечение body-parser , которое req.body отправленные значения формы в req.body в ваших обработчиках маршрута. Добавьте его в конец массива middlewares :

 // server.js const bodyParser = require('body-parser') const middlewares = [ // ... bodyParser.urlencoded() ] 

Это обычное соглашение для форм отправлять данные обратно на тот же URL-адрес, который использовался в начальном GET-запросе. Давайте сделаем это здесь и обработаем POST /contact для обработки ввода пользователя.

Давайте сначала посмотрим на неверное представление. Если они недействительны, нам нужно вернуть отправленные значения в представление, чтобы им не нужно было повторно вводить их вместе с сообщениями об ошибках, которые мы хотим отобразить:

 router.get('/contact', (req, res) => { res.render('contact', { data: {}, errors: {} }) }) router.post('/contact', (req, res) => { res.render('contact', { data: req.body, // { message, email } errors: { message: { msg: 'A message is required' }, email: { msg: 'That email doesn't look right' } } }) }) 

Если есть какие-либо ошибки проверки, мы сделаем следующее:

  • отображать ошибки в верхней части формы
  • установить входные значения на то, что было отправлено на сервер
  • отображать встроенные ошибки под входами
  • добавить form-field-invalid класс поля формы к полям с ошибками.
 <!-- views/contact.ejs --> <div class="form-header"> <% if (Object.keys(errors).length === 0) { %> <h2>Send us a message</h2> <% } else { %> <h2 class="errors-heading">Oops, please correct the following:</h2> <ul class="errors-list"> <% Object.values(errors).forEach(error => { %> <li><%= error.msg %></li> <% }) %> </ul> <% } %> </div> <form method="post" action="/contact" novalidate> <div class="form-field <%= errors.message ? 'form-field-invalid' : '' %>"> <label for="message">Message</label> <textarea class="input" id="message" name="message" rows="4" autofocus><%= data.message %></textarea> <% if (errors.message) { %> <div class="error"><%= errors.message.msg %></div> <% } %> </div> <div class="form-field <%= errors.email ? 'form-field-invalid' : '' %>"> <label for="email">Email</label> <input class="input" id="email" name="email" type="email" value="<%= data.email %>" /> <% if (errors.email) { %> <div class="error"><%= errors.email.msg %></div> <% } %> </div> <div class="form-actions"> <button class="btn" type="submit">Send</button> </div> </form> 

Отправьте форму по адресу http://localhost:3000/contact чтобы увидеть это в действии. Это все, что нам нужно на стороне обзора.

Валидация и дезинфекция

Для проверки и дезинфекции данных с помощью библиотеки validator.js есть удобный промежуточный программный express-validator Давайте включим его в наш массив middlewares :

 // server.js const validator = require('express-validator') const middlewares = [ // ... validator() ] 

Проверка

С предоставленными валидаторами мы можем легко проверить, что сообщение и действительный адрес электронной почты были предоставлены:

 // routes.js const { check, validationResult } = require('express-validator/check') router.post('/contact', [ check('message') .isLength({ min: 1 }) .withMessage('Message is required'), check('email') .isEmail() .withMessage('That email doesn't look right') ], (req, res) => { const errors = validationResult(req) res.render('contact', { data: req.body, errors: errors.mapped() }) }) 

Санитарная

С предоставленными дезинфицирующими средствами мы можем урезать пробелы от начала и конца значений и нормализовать электронную почту в последовательный образец. Это может помочь удалить дубликаты контактов, создаваемых немного разными входами. Например, ' [email protected]' и « '[email protected] ' будут '[email protected]' в '[email protected]' .

Дезинфицирующие средства могут быть просто прикованы к концу валидаторов:

 const { matchedData } = require('express-validator/filter') router.post('/contact', [ check('message') .isLength({ min: 1 }) .withMessage('Message is required') .trim(), check('email') .isEmail() .withMessage('That email doesn't look right') .trim() .normalizeEmail() ], (req, res) => { const errors = validationResult(req) res.render('contact', { data: req.body, errors: errors.mapped() }) const data = matchedData(req) console.log('Sanitized:', data) }) 

Функция matchedData возвращает вывод дезинфицирующих средств на наш вход.

Действительная форма

Если есть ошибки, нам нужно перерисовать представление. Если нет, нам нужно сделать что-то полезное с данными, а затем показать, что отправка прошла успешно. Как правило, человек перенаправляется на страницу успеха и отображается сообщение.

HTTP не имеет состояния, поэтому вы не можете перенаправить на другую страницу и передавать сообщения без помощи куки-файла сеанса для сохранения этого сообщения между HTTP-запросами. «Флэш-сообщение» — это имя, данное единовременному сообщению такого рода, которое мы хотим сохранить при перенаправлении, а затем исчезнуть.

Есть три промежуточных программного обеспечения, которые мы должны включить, чтобы подключить это:

 const cookieParser = require('cookie-parser') const session = require('express-session') const flash = require('express-flash') const middlewares = [ // ... cookieParser(), session({ secret: 'super-secret-key', key: 'super-secret-cookie', resave: false, saveUninitialized: false, cookie: { maxAge: 60000 } }), flash() ] 

req.flash(type, message) программное обеспечение express-flash добавляет req.flash(type, message) который мы можем использовать в наших обработчиках маршрута:

 // routes router.post('/contact', [ // validation ... ], (req, res) => { const errors = validationResult(req) if (!errors.isEmpty()) { return res.render('contact', { data: req.body, errors: errors.mapped() }) } const data = matchedData(req) console.log('Sanitized: ', data) // Homework: send sanitized data in an email or persist in a db req.flash('success', 'Thanks for the message! I'll be in touch :)') res.redirect('/') }) 

req.locals ПО express-flash добавляет messages в req.locals которым имеют доступ все представления:

 <!-- views/index.ejs --> <% if (messages.success) { %> <div class="flash flash-success"><%= messages.success %></div> <% } %> <h1>Working With Forms in Node.js</h1> 

Теперь вы должны быть перенаправлены в представление индекса и увидеть сообщение об успешном завершении, когда форма отправлена ​​с действительными данными. Ура! Теперь мы можем развернуть это в производство и отправлять сообщения принцу Нигерии.

Соображения безопасности

Если вы работаете с формами и сессиями в Интернете, вам необходимо знать о распространенных пробелах в безопасности веб-приложений. Лучший совет по безопасности, который мне дали, — «Никогда не доверяй клиенту!»

TLS через HTTPS

Всегда используйте шифрование TLS через https:// при работе с формами, чтобы отправленные данные шифровались при отправке через Интернет. Если вы отправляете данные формы через http:// , они отправляются в виде простого текста и могут быть видны всем, кто подслушивает эти пакеты, когда они передаются через Интернет.

Носи свой шлем

Есть аккуратное промежуточное ПО под названием шлем, которое добавляет некоторую защиту от заголовков HTTP. Лучше всего указывать прямо в верхней части вашего промежуточного программного обеспечения и очень легко включить:

 // server.js const helmet = require('helmet') middlewares = [ helmet() // ... ] 

Подделка межсайтовых запросов (CSRF)

Вы можете защитить себя от подделки межсайтовых запросов , создав уникальный токен, когда пользователю предоставляется форма, и затем проверив этот токен перед обработкой данных POST. Здесь вам также поможет промежуточное ПО:

 // server.js const csrf = require('csurf') middlewares = [ // ... csrf({ cookie: true }) ] 

В GET-запросе мы генерируем токен:

 // routes.js router.get('/contact', (req, res) => { res.render('contact', { data: {}, errors: {}, csrfToken: req.csrfToken() }) }) 

А также в ответе на ошибки проверки:

 router.post('/contact', [ // validations ... ], (req, res) => { const errors = validationResult(req) if (!errors.isEmpty()) { return res.render('contact', { data: req.body, errors: errors.mapped(), csrfToken: req.csrfToken() }) } // ... }) 

Тогда нам просто нужно включить токен в скрытый ввод:

 <!-- view/contact.ejs --> <form method="post" action="/contact" novalidate> <input type="hidden" name="_csrf" value="<%= csrfToken %>"> <!-- ... --> </form> 

Это все, что требуется.

Нам не нужно изменять наш обработчик POST-запросов, поскольку для всех POST-запросов теперь требуется действительный токен от промежуточного программного обеспечения csurf . Если действительный токен CSRF не предоставлен, будет ForbiddenError ошибка ForbiddenError , которая может быть обработана обработчиком ошибок, определенным в конце server.js .

Вы можете проверить это самостоятельно, отредактировав или удалив токен из формы с помощью инструментов разработчика вашего браузера и отправив его.

Межсайтовый скриптинг (XSS)

Вы должны соблюдать осторожность при отображении данных, представленных пользователем, в представлении HTML, поскольку это может открыть вас для межсайтовых сценариев (XSS) . Все языки шаблонов предоставляют разные методы для вывода значений. EJS <%= value %> выводит экранированное значение HTML, чтобы защитить вас от XSS, тогда как <%- value %> выводит необработанную строку.

Всегда используйте экранированный вывод <%= value %> при работе с пользовательскими значениями. Используйте необработанные результаты только тогда, когда вы уверены, что это безопасно.

Загрузка файлов

Загрузка файлов в формах HTML — это особый случай, для которого требуется тип кодирования "multipart/form-data" . См . Руководство MDN по отправке данных формы для получения более подробной информации о том, что происходит с многокомпонентной отправкой формы.

Вам понадобится дополнительное промежуточное программное обеспечение для обработки нескольких загрузок. Есть пакет Express с именем multer который мы будем использовать здесь:

 // routes.js const multer = require('multer') const upload = multer({ storage: multer.memoryStorage() }) router.post('/contact', upload.single('photo'), [ // validation ... ], (req, res) => { // error handling ... if (req.file) { console.log('Uploaded: ', req.file) // Homework: Upload file to S3 } req.flash('success', 'Thanks for the message! I'll be in touch :)') res.redirect('/') }) 

Этот код инструктирует multer загружать файл из поля «photo» в память и предоставляет объект File в req.file который мы можем проверить или обработать в дальнейшем.

Последнее, что нам нужно, это добавить атрибут enctype и наш файл ввода:

 <form method="post" action="/contact?_csrf=<%= csrfToken %>" novalidate enctype="multipart/form-data"> <input type="hidden" name="_csrf" value="<%= csrfToken %>"> <div class="form-field <%= errors.message ? 'form-field-invalid' : '' %>"> <label for="message">Message</label> <textarea class="input" id="message" name="message" rows="4" autofocus><%= data.message %></textarea> <% if (errors.message) { %> <div class="error"><%= errors.message.msg %></div> <% } %> </div> <div class="form-field <%= errors.email ? 'form-field-invalid' : '' %>"> <label for="email">Email</label> <input class="input" id="email" name="email" type="email" value="<%= data.email %>" /> <% if (errors.email) { %> <div class="error"><%= errors.email.msg %></div> <% } %> </div> <div class="form-field"> <label for="photo">Photo</label> <input class="input" id="photo" name="photo" type="file" /> </div> <div class="form-actions"> <button class="btn" type="submit">Send</button> </div> </form> 

К сожалению, нам также нужно было включить _csrf в качестве параметра GET, чтобы промежуточное программное обеспечение csurf мяч и не теряло наш токен во время составной отправки.

Попробуйте загрузить файл, вы должны увидеть объекты File зарегистрированные в консоли.

Заполнение файловых входов

В случае ошибок проверки мы не можем повторно заполнить ввод файлов, как мы это делали для ввода текста. Общий подход к решению этой проблемы включает в себя следующие этапы:

  • загрузка файла во временную папку на сервере
  • показывая миниатюру и имя файла вложенного файла
  • добавление JavaScript в форму, чтобы позволить людям удалить выбранный файл или загрузить новый
  • перемещение файла в постоянное место, когда все в порядке.

Из-за дополнительных сложностей работы с многочастной загрузкой и загрузкой файлов они часто хранятся в отдельных формах.

Спасибо за прочтение

Надеюсь, вам понравилось узнавать о формах HTML и о том, как работать с ними в Express и Node.js. Вот краткий обзор того, что мы рассмотрели:

  • отображение пустой формы в ответ на запрос GET
  • обработка представленных данных POST
  • отображение списка ошибок, встроенных ошибок и представленных данных
  • проверка представленных данных с помощью валидаторов
  • очистка представленных данных с помощью дезинфицирующих средств
  • передача сообщений через перенаправления с помощью флеш-сообщения
  • защитить себя от атак типа CSRF и XSS
  • обработка загрузки файлов в многочастной форме.

Дайте нам знать, как вы попали в комментарии!