Обратите внимание, прежде чем мы начнем - это пошаговое руководство для простого приложения Express, в котором вы можете входить и выходить из системы. Я буду использовать Express для настройки сервера, Mongoose для работы с базой данных, мой собственный API для обработки логина пользователя, bcrypt для шифрования паролей и JWT для аутентификации пользователей.
Я всего лишь студент веб-разработки, и я пишу это, чтобы помочь другим в их первых шагах к обработке логинов, и я считаю, что это будет хорошей отправной точкой.
Теперь, когда мы это прояснили, давайте продолжим и удачного кодирования!
Первое, что нам нужно сделать, это инициализировать npm и запросить все пакеты, которые мы собираемся использовать для приложения.
npm init npm i express mongoose body-parser cors bcrypt express-jwt jsonwebtoken validator morgan --save npm i babel-cli babel-preset-es2015 browser-sync gulp gulp-babel gulp-clean-css gulp-nodemon gulp-sass gulp-uglify gulp-imagemin --save-dev
Что касается зависимостей разработчиков, я использую Babel, потому что я пишу ECMA6, но если это не так, вы можете опустить babel. Для запуска задач я использую Gulp.
Затем нам нужно создать индексные файлы вместе с gulpfile.
touch index.js index.html gulpfile.js
Заполните gulpfile.js этим кодом. Этот файл gulp позаботится о нашем сервере Node, SASS и ES6.
Теперь нам нужно создать файлы ES6 JS и SASS в папке src - мы будем писать код в этой папке (gulp скомпилирует эти файлы в общую папку), а также файлы ES5 JS и CSS в общей папке (которые будут обслуживать наш index. html и отобразить в браузере). (Боковое примечание: убедитесь, что вы редактируете файлы только в папке src. Если вы редактируете общую папку, а затем также вносите изменения в src, gulp изменит public, так что он будет таким же, как src, и вы потеряете все свои изменения. )
mkdir public src mkdir public/js public/css src/js src/scss touch public/js/app.js public/css/style.css src/js/app.js src/scss/style.scss
Теперь нам нужно настроить сервер и базу данных. Мы будем использовать Express для сервера и Mongoose для базы данных. Мы установим их в нашем index.js в корневом каталоге. Сначала нам нужно потребовать все пакеты, которые мы собираемся использовать с Express и Mongoose, а затем установить все промежуточное ПО.
const express = require('express'); const morgan = require('morgan'); const bodyParser = require('body-parser'); const cors = require('cors'); const mongoose = require('mongoose'); const port = process.env.PORT || 3000; const databaseUrl = 'mongodb://localhost/authentication-jwt'; const app = express(); mongoose.connect(databaseUrl); app.use(morgan('dev')); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); app.use(cors()); app.use(express.static(`${__dirname}/public`)); app.use(express.static(`${__dirname}/bower_components`)); // for now you can comment out the line above // we're gonna install bower later on app.listen(port, () => console.log(`Express running on port ${port}`));
Если мы попытаемся запустить приложение на этом этапе (используя команду gulp в нашем терминале), мы получим сообщение об ошибке: «Cannot GET /», которое связано с тот факт, что мы еще не установили маршрут для нашей домашней страницы. Итак, нам нужно создать две папки в нашем корневом каталоге: config - который будет содержать наш маршрутизатор, и контроллеры - содержащий логику маршрутизаторов.
mkdir config controllers touch config/routes.js touch controllers/statics.js
Нам нужно сделать index.html нашей домашней страницей в statics.js.
module.exports = { home: staticsHome }; const path = require('path'); function staticsHome(req, res){ return res.sendFile(path.join(__dirname, '../index.html')); }
И нам нужно установить обработчик маршрута для нашей домашней страницы в routes.js.
const express = require('express'); const router = express.Router(); const statics = require('../controllers/statics'); router.route('/').get(statics.home); module.exports = router;
Теперь нам нужно потребовать маршрутизатор и использовать его в index.js. Перед нашим методом app.listen необходимо вставить следующий код.
const router = require('./config/routes'); app.use(router);
На следующем этапе мне нравится добавлять базовые компоненты на страницу, а именно панель навигации, желательно с использованием какой-либо структуры для быстрого старта, подключать CSS и JS к индексу и тестировать его.
Я буду использовать Materialize для этого проекта и установлю его с помощью Bower. (Вы также должны установить express.static для bower_components в index.js - мы писали этот код в комментарии ранее.)
bower init bower install materialize
Я добавил панель навигации Materialize в index.html, добавил background: #fafafa в тело нашего style.scss и console.log ('работает') в app.js в наша папка src / js. Последнее, что нам нужно сделать перед тестированием, - это связать всю нашу таблицу стилей и скрипты в заголовке index.html.
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/materialize/dist/css/materialize.min.css"> <script src="/jquery/dist/jquery.min.js" charset="utf-8"></script> <script src="/materialize/dist/js/materialize.min.js" charset="utf-8"></script> <script src="/js/app.js" charset="utf-8"></script>
Теперь, если вы запустите gulp в терминале, вы должны получить что-то вроде этого:
Следующим шагом является установка навигационных ссылок для входа и регистрации. Когда мы нажимаем на них, они должны отображать форму. Мы создаем одностраничное приложение, поэтому нам не нужно устанавливать обработчик маршрута. Мы будем показывать формы динамически с помощью JS.
Мы должны установить прослушиватель событий для щелчков по нашей ссылке регистрации и входа в систему. Мы можем добавить к ним класс регистрации и входа, а все остальное обработать в app.js в папке src / js. Полные функции с формами html вы можете найти здесь.
function init() { $('.registration').on('click', registerForm); $('.login').on('click', loginForm); } function registerForm(e) { e.preventDefault(); $('.container').html(` ADD YOUR FORM HTML HERE `); } function loginForm(e) { e.preventDefault(); $('.container').html(` ADD YOUR FORM HTML HERE `); } $(init);
Теперь, когда вы нажали «Войти» или «Зарегистрироваться», вы должны увидеть форму. Следующим шагом является создание пользователя, и для этого нам нужно создать новую модель пользователя. Нам понадобится новая папка с именем models и файл user.js, где мы определим новую схему мангуста. Итак, models / user.js будет выглядеть так (часть I):
const mongoose = requier('mongoose'); const bcrypt = require('bcrypt'); const validator = require('validator'); const userSchema = new mongoose.Schema({ username: { type: String, unique: true, required: true }, email: { type: String, unique: true, required: true }, passwordHash: { type: String, unique: true, required: true } }); userSchema.virtual('password').set(setPassword); userSchema.virtual('passwordConfirmation').set(setPasswordConfirmation); function setPassword(password) { this._password = password; this.passwordHash = bcrypt.hashSync(password, bcrypt.genSaltSync(8)); } function setPasswordConfirmation(passwordConfirmation) { this._passwordConfirmation = passwordConfirmation; }
Для первой части нам нужно создать новый объект схемы, который будет обрабатывать новые записи в базе данных. Я назвал свою userSchema и установил для нее требуемые свойства имени пользователя, электронной почты и passwordHash, которые будут хранить зашифрованный пароль, который пользователь предоставляет во время регистрации. Мы собираемся получить пароль пользователя с помощью метода .virtual () и установить его с помощью функции setPassword, которая принимает один аргумент -password- encrypts он использует пакет bcrypt и сохраняет его в passwordHash. Мы также временно храним _passwordConfirmation, чтобы позже сравнить его с _password. _password и _passwordConfirmation не будут сохранены как только новый пользователь создается из-за того, как работает .virtual () - он только прослушивает свойства во время регистрации и удаляет их при сохранении нового пользователя.
userSchema.path('passwordHash').validate(validatePasswordHash); function validatePasswordHash() { if (this.isNew) { if (!this._password) { return this.invalidate('password', 'A password is required.'); } if (this._password.length < 6) { return this.invalidate('password', 'Password must be at least 6 characters.'); } if (this._password !== this._passwordConfirmation) { return this.invalidate('password', 'Passwords do not match.'); } } }
Еще одна функция, которую мы должны добавить, - это validatePasswordHash, которая проверяет ввод пароля и делает недействительным всего пользователя, если пароли этого не делают: а) пароль вообще не вводится, б) пароль меньше 6 символов, в) пароль и поле подтверждения пароля не совпадают. Invalidates прерывает процесс сохранения, и любая попытка сохранить этого пользователя недействительна.
Мы также можем добавить проверку электронной почты:
userSchema.path('email').validate(validateEmail); function validateEmail(email) { if (!validator.isEmail(email)) { return this.invalidate('email', 'A valid e-mail address is required.'); } }
Наконец, нам нужно добавить еще один метод в userSchema-one для проверки пароля для входа в систему, который будет использоваться позже, и module.exports. И это все, что касается пользовательской модели.
userSchema.methods.validatePassword = validatePassword; function validatePassword(password) { return bcrypt.compareSync(password, this.passwordHash); } module.exports = mongoose.model('User', userSchema);
Теперь, когда у нас есть формы и модель пользователя, следующим шагом будет обработка регистрации и входа пользователей. Когда кто-то регистрируется, нам нужно создать новую запись в базе данных с его данными. На этом этапе мы также можем сгенерировать токен JWT для последующей аутентификации. Токен JWT - это строка, которая хранится в локальном хранилище браузера и позволяет пользователям получать доступ к странице без необходимости повторного входа в систему. Подумайте, как вы можете открыть Twitter и вам не придется входить в систему, если вы не вышли из системы во время последнего сеанса. Если вы хотите узнать больше о JWT, их сайт - хорошая отправная точка. Создайте Authentication.js в папке controllers, которая будет содержать этот код:
const User = require('../models/user'); const jwt = require('jsonwebtoken'); const secret = 'Something top secret.'; function authenticationsRegister(req, res){ User.create(req.body.user, (err, user) => { if (err) return res.status(500).json({message: 'Something went wrong.'}); const token = jwt.sign(user._id, secret, { expiresIn: 60*60*24 }); return res.status(201).json({ message: `Welcome ${user.username}!`, user, token }); }); }
Как всегда, нам сначала нужно запросить пакеты, которые мы собираемся использовать. Затем я определил функцию под названием AuthenticationRegister, которая обрабатывает новых пользователей. User.create () примет форму и создаст нового пользователя на основе отправленной формы (содержащейся в теле запроса). Если произойдет какая-либо ошибка, мы отправим код состояния 500 и сообщение «Что-то пошло не так». Переменная токена сгенерирует новый токен для этого пользователя. Функция jwt.sign принимает три аргумента: user._id, который является идентификатором, который пользователь назначил ему в базе данных (мангуст делает это автоматически), secret - это строка. который мы определили ранее в файле, это случайная строка, которая помогает создавать и кодировать уникальный токен для каждого пользователя, expiresIn не требует пояснений - этот токен действителен в течение 24 часов. Когда пользователь создается, мы отправляем обратно код состояния 201 (создан) и json, содержащий сообщение, объект пользователя и токен jwt.
Теперь нам нужно обработать логин уже существующего пользователя.
function authenticationsLogin(req, res) { User.findOne( {email: req.body.email}, (err, user) => { if (err) return res.status(500).json({ message: 'Something went wrong.'}); if (!user || !user.validatePassword(req.body.password)) { return res.status(401).json({ message: 'Unauthorised.'}); } const token = jwt.sign(user._id, secret, { expiresIn: 60*60*24}); return res.status(200).json({ message: 'Welcome back', user, token }); }); } module.exports = { register: authenticationsRegister, login: authenticationsLogin };
Когда пользователь отправляет форму входа в систему, функция authenticationLogin будет искать этого пользователя в базе данных на основе предоставленного адреса электронной почты. Если пользователь не найден или введен неправильный пароль, функция возвращает код состояния 401 (неавторизованный). Мы проверяем пароль с помощью функции validatePassword (определенной в модели пользователя), которая возвращает истину или ложь, если пароль совпадает с passwordHash или нет (метод compareSync bcrypt сравнивает строку пароля со строкой passwordHash). Наконец, если все прошло успешно, мы возвращаем код состояния 200 и json с сообщением, пользователем и токеном jwt.
Теперь, когда мы определили функции аутентификации, нам нужно создать для них маршруты. Создайте apiRoutes.js в папке config.
const express = require('express'); const router = express.Router(); const authentication = require('../controllers/authentications'); router.route('/register') .post(authentication.register); router.route('/login') .post(authentication.login); module.exports = router;
Прежде чем мы сможем проверить, работают ли наши маршруты API, нам нужно добавить несколько строк кода в index.js.
const apiRouter = require('./config/apiRoutes'); const expressJWT = require('express-jwt'); const secret = 'Something top secret.'; app.use('/api', expressJWT({ secret }) .unless({ path: [ { url: '/api/register', methods: ['POST'] }, { url: '/api/login', methods: ['POST'] } ] })); app.use(jwtErrorHandler); function jwtErrorHandler(err, req, res, next){ if (err.name !== 'UnauthorizedError') return next(); return res.status(401).json({ message: 'Unauthorized request.' }); } app.use('/api', apiRouter);
Я использую Insomnia для тестирования API, поэтому, как только я сохраню весь код, я могу открыть его и создать нового пользователя, чтобы посмотреть, все ли работает как надо:
Я протестировал маршрут «/ api / register», и, как вы можете видеть, API вернул сообщение, объект пользователя и токен так же, как мы настроили ранее. Мы также можем проверить токен, и если он работает, перейдя в заголовок и добавив Authorization в Content-type, а в application / json мы добавим Bearer, за которым следует значение токена без цитаты вроде этого: (примечание: это авторизация, а не аутентификация, как на картинке - мои извинения)
Теперь, если мы перейдем на главную страницу и изменим URL-адрес на «/ api / login» и введем наши данные для входа, мы увидим, что он вернул нам сообщение с приветствием.
Это означает, что наша регистрация, логин и аутентификация работают! Теперь нам нужно только добавить его во фронтенд и подключить к формам.
(В этот момент я подумал, что заслужил перерыв, и несколько часов играл в Тени Мордора, убил несколько сотен уруков, и теперь я готов продолжить. Так что, если вы хотите сделать паузу и пообедать, я не буду судить. Нет? Хорошо, давайте продолжим!)
const apiUrl = 'http://localhost:3000/api'; function init() { $('.registration').on('click', registerForm); $('.login').on('click', loginForm); $('body').on('submit', 'form', submittedForm); } function submittedForm(e) { if (e) e.preventDefault(); $.ajax({ url: `${apiUrl}${$(this).attr('action')}`, method: $(this).attr('method'), data: $(this).serialize(), beforeSend: setRequestHeader }).done((data) => { if (data.token) setToken(data.token); }); } function setToken(token) { return window.localStorage.setItem('token', token); } function setRequestHeader(xhr) { return xhr.setRequestHeader('Authorization', `Bearer ${getToken()}`); } function getToken() { return window.localStorage.getItem('token'); }
Слушатель отправки необходим для форм с функцией обратного вызова submitForm. Мы не можем добавить прослушиватель событий в форму напрямую, поскольку они не были частью DOM, когда страница была загружена, и обратный вызов не будет выполняться при отправке формы. Делегирование событий может справиться с этим - сначала мы добавляем прослушиватель событий к родительскому элементу, который изначально был частью DOM - в данном случае body - затем мы добавляем функцию .on () и вызываем событие - 'submit' - второй аргумент - это элемент, к которому мы хотим привязать слушателя - 'form', а последний аргумент - это функция обратного вызова.
Внутри функции SubmittedForm находится запрос AJAX. Мы отправляем данные формы в базу данных методом post. Частью запроса AJAX является функция setRequestHeader beforeSend, которая добавляет токен jwt в заголовок запроса. Функция обратного вызова для запроса AJAX - это функция setToken, которая добавляет токен jwt в локальное хранилище браузера.
Последняя часть приложения предназначена для обработки выхода и внешнего вида страницы в зависимости от того, вошли вы в систему или вышли из нее. Нам нужно добавить класс «loggedIn» к навигационной ссылке .logout и класс «loggedOut» к навигационным ссылкам .register и .login. Затем мы добавляем прослушиватель кликов в .logout и записываем этот код в app.js:
function removeToken() { return window.localStorage.clear(); } function loggedInState() { $('.loggedIn').show(); $('.loggedOut').hide(); } function loggedOutState() { $('.loggedIn').hide(); $('.loggedOut').show(); } function logOut(e) { e.preventDefault(); loggedOutState(); loginForm(); removeToken(); }
Наша функция init теперь должна выглядеть так:
function init() { $('.registration').on('click', registerForm); $('.login').on('click', loginForm); $('.logout').on('click', logOut); $('body').on('submit', 'form', submittedForm); if (getToken()) { loggedInState(); } else { loggedOutState(); } }
На этом этапе ваше приложение должно иметь возможность регистрировать пользователя, обрабатывать вход и выход из системы и изменять интерфейс, если вы вошли в систему или вышли из нее.
Надеюсь, вы нашли это руководство полезным, и если у вас есть вопросы, не стесняйтесь их задавать!
Если вы хотите узнать больше о защищенных серверах, я рекомендую 9 советов по безопасности, чтобы не дать экспресс-почтой.
До следующего раза и счастливого кодирования!