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

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

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

От автора: если вы создаете веб-приложение, то в первые дни, вероятно, сталкиваетесь с необходимостью создавать HTML-формы. Они являются большой частью веб experience, и могут быть довольно сложными. И здесь может прийти на помощь Node js Express.

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

отображение пустой HTML-формы в ответ на исходный GET запрос

пользователь, отправляющий форму с данными в POST запросе

проверка как в клиенте, так и на сервере

повторное отображение формы, заполненной экранированными данными и сообщениями об ошибках, если они недействительны

Действия с очищенными данными на сервере, если они действительны

перенаправление пользователя или отображение успешного сообщения после обработки данных.

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

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

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

Настройка

Убедитесь, что у вас установлена последняя версия 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 в обработчиках маршрутов. Добавьте его в конец middlewares массива:

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

Это обычное соглашение форм для обратных POST-данных к тому же 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, чтобы увидеть её в действии. Это все, что нам нужно от представления.

Валидация и защита

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

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

Валидация

При наличии валидаторов мы можем легко проверить, что были предоставлены сообщение и действующий e-mail:

// 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]’.

Эти средства могут быть просто привязаны к концу валидаторов:

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 не имеет статуса, поэтому вы не можете перенаправлять на другую страницу и передавать сообщения без помощи cookie сеанса, чтобы сохранить это сообщение между 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()
]

Средство 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('/')
})

Средство 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://, они отправляются в виде обычного текста и могут быть видны любому, кто заглядывает в эти пакеты во время путешествия по Интернету.

Носите шлем

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

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

Подделка кросс-сайт запросов/Cross-site Request Forgery (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, которую может обработать обработчик ошибок, определенный в конце server.js.

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

Межсайтовый скриптинг/Cross-site Scripting (XSS)

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

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

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

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

Для обработки многостраничных загрузок вам потребуется дополнительное промежуточное программное обеспечение. Там есть экспресс пакет 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, как загрузить файл в поле «фото» в память, и выставляет 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 в форму, чтобы люди могли удалить выбранный файл или загрузить новый

перемещение файла в постоянное место, когда все действительно.

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

Спасибо за внимание

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

отображение пустой формы в ответ на запрос GET

обработка представленных данных POST

отображение списка ошибок, встроенных ошибок и представленных данных

проверка представленных данных с помощью валидаторов

очистка представленных данных с помощью средств чистки

передача сообщений через перенаправление с помощью флэш-сообщения

защита от атак типа CSRF и XSS

загрузка файлов в сообщениях с несколькими формами.

Автор: Mark Brown

Источник: https://www.sitepoint.com/

Редакция: Команда webformyself.