- PVSM.RU - https://www.pvsm.ru -
Задача авторизации возникает практически в каждом Node.js проекте, однако, чтобы ее правильно настроить, необходимо подключить большое количество модулей и собрать кучу информации из разных источников.
В этой статье я опишу полноценное решение по авторизации на основе JSON Web Token (JWT) для Node.js и Koa с хранением хэшей паролей в MongoDB. От читателя ожидаются базовые знания Node.js и принципов работы с MongoDB через Mongoose.
Несколько слов, о чем конкретно пойдет речь и почему.
Почему Коа. Не смотря на значительно большую популярность фреймворка Express, Koa [1]предоставляет возможность писать приложения используя современный синтаксис async/await. Использование async/await вместо callback’ов является достаточно большим стимулом, чтобы присмотреться к этому фреймворку.
Почему JWT. Подход к авторизации с помощью сессий можно уже назвать устаревшим, так как он не позволяет использовать его в мобильных приложениях и там, где нет поддержки cookies. Также проблемы с сессиями могут возникнуть в кластерных системах. JWT авторизация не имеет этих недостатков, и обладает еще рядом дополнительных преимуществ. Более подробно про JWT можно прочитать тут. [2]
В статье будет рассмотрено полноценное решение по авторизации с использованием:
Чтобы сохранить образовательную ценность статьи в коде не будет расширенных проверок на ошибки и исключения, которые часто делают код менее понятным. Поэтому перед использованием примеров кода в продакшене, надо поработать над обработкой ошибок и контролем входных данных от клиента.
1. Подключаем Koa. В отличие от Express, Koa является более легким фреймворком и поэтому, обычно, используется с рядом дополнительных модулей.
const Koa = require('koa'); // ядро
const Router = require('koa-router'); // маршрутизация
const bodyParser = require('koa-bodyparser'); // парсер для POST запросов
const serve = require('koa-static'); // модуль, который отдает статические файлы типа index.html из заданной директории
const logger = require('koa-logger'); // опциональный модуль для логов сетевых запросов. Полезен при разработке.
const app = new Koa();
const router = new Router();
app.use(serve('public'));
app.use(logger());
app.use(bodyParser());
2. Подключаем Passport.js [3]. Passport.js позволяет гибко настраивать авторизацию, используя разные механизмы, которые называются Стратегиями (локальная, социальные сети д.р.). В настоящий момент библиотека насчитывает более 300 вариантов стратегий.
const passport = require('koa-passport'); //реализация passport для Koa
const LocalStrategy = require('passport-local'); //локальная стратегия авторизации
const JwtStrategy = require('passport-jwt').Strategy; // авторизация через JWT
const ExtractJwt = require('passport-jwt').ExtractJwt; // авторизация через JWT
app.use(passport.initialize()); // сначала passport
app.use(router.routes()); // потом маршруты
const server = app.listen(3000);// запускаем сервер на порту 3000
3. Подключаем работу с JWT. В двух словах JWT — это просто JSON в котором может храниться, например, email пользователя. Этот JSON подписывается секретным ключом, что не позволяет этот email изменить, хотя позволяет его прочитать.
Таким образом, получая с клиента JWT вы уверены, что к вам пришел именно тот пользователь, за которого он себя выдает (при условии, что его JWT не был кем-то украден, но это уже совсем другая история).
const jwtsecret = "mysecretkey"; // ключ для подписи JWT
const jwt = require('jsonwebtoken'); // аутентификация по JWT для hhtp
const socketioJwt = require('socketio-jwt'); // аутентификация по JWT для socket.io
4. Подключаем socket.io. [4] В двух словах socket.io — это модуль для работы приложений, которые реагируют на изменения происходящие на сервере, например его можно использовать для чата. Если сервер и браузер поддерживают протокол WebSockets, то socket.io будет использовав его, иначе он поищет другие механизмы реализации двустороннего общения браузера с сервером.
const socketIO = require('socket.io');
5. Подключаем MongoDB для хранения объектов пользователей.
const mongoose = require('mongoose'); // стандартная прослойка для работы с MongoDB
const crypto = require('crypto'); // модуль node.js для выполнения различных шифровальных операций, в т.ч. для создания хэшей.
Объект пользователя (user) будет состоять из его имени, e-mail и хэша пароля.
Для превращения пароля, получаемого из POST запроса в хэш, который будет храниться в базе применяется концепция виртуальных полей. Виртуальное поле — это поле, которое есть в модели Mongoose, но которого нет в базе MongoDB.
mongoose.Promise = Promise; // Просим Mongoose использовать стандартные Промисы
mongoose.set('debug', true); // Просим Mongoose писать все запросы к базе в консоль. Удобно для отладки кода
mongoose.connect('mongodb://localhost/test'); // Подключаемся к базе test на локальной машине. Если базы нет, она будет создана автоматически.
Создаем схему и модель для Пользователя:
const userSchema = new mongoose.Schema({
displayName: String,
email: {
type: String,
required: 'Укажите e-mail',
unique: 'Такой e-mail уже существует'
},
passwordHash: String,
salt: String,
}, {
timestamps: true
});
userSchema.virtual('password')
.set(function (password) {
this._plainPassword = password;
if (password) {
this.salt = crypto.randomBytes(128).toString('base64');
this.passwordHash = crypto.pbkdf2Sync(password, this.salt, 1, 128, 'sha1');
} else {
this.salt = undefined;
this.passwordHash = undefined;
}
})
.get(function () {
return this._plainPassword;
});
userSchema.methods.checkPassword = function (password) {
if (!password) return false;
if (!this.passwordHash) return false;
return crypto.pbkdf2Sync(password, this.salt, 1, 128, 'sha1') == this.passwordHash;
};
const User = mongoose.model('User', userSchema);
Для более глубокого понимания механизма работы с хэшами паролей можно почитать про команду pbkdf2Sync в доке [5] по Node.js
Процесс авторизации пользователя выглядит следующим образом:
Шаг 1. Новый пользователь регистрируется, и создается запись о нем в базе MongoDB.
Шаг 2. Пользователь логинится с паролем на сайте и при успешном вводе логина и пароля получает JWT.
Шаг3. Пользователь заходит на произвольный ресурс, отсылает свой JWT, по которому и авторизуется уже без ввода пароля.
Механизм настройки Passport.js состоит из двух этапов:
Этап 1. Настройка Стратегий. Стратегия при успешной авторизации возвращает объект user, описанный ранее в схеме userSchema.
Этап 2. Использование полученного на этапе 1 объекта user для последующих действий, например, создания для него JWT.
Настраиваем Passport Local Strategy. Более подробно, как работает стратегия можно прочитать на тут [6].
passport.use(new LocalStrategy({
usernameField: 'email',
passwordField: 'password',
session: false
},
function (email, password, done) {
User.findOne({email}, (err, user) => {
if (err) {
return done(err);
}
if (!user || !user.checkPassword(password)) {
return done(null, false, {message: 'Нет такого пользователя или пароль неверен.'});
}
return done(null, user);
});
}
)
);
Настраиваем Passport JWT Strategy. Более подробно, как работает стратегия можно прочитать на тут [7].
// Ждем JWT в Header
const jwtOptions = {
jwtFromRequest: ExtractJwt.fromAuthHeader(),
secretOrKey: jwtsecret
};
passport.use(new JwtStrategy(jwtOptions, function (payload, done) {
User.findById(payload.id, (err, user) => {
if (err) {
return done(err)
}
if (user) {
done(null, user)
} else {
done(null, false)
}
})
})
);
Мы создадим REST API, который будет работать с объектом user.
API будет состоять из трех endpoints, соответствующих трем Шагам процесса авторизации, описанному выше.
Post запрос на /user – создает нового пользователя. Обычно этот API вызывается при регистрации нового пользователя. В теле запроса мы ожидаем JSON с именем, почтой и паролем пользователя.
router.post('/user', async(ctx, next) => {
try {
ctx.body = await User.create(ctx.request.body);
}
catch (err) {
ctx.status = 400;
ctx.body = err;
}
});
Post запрос на /login создает JWT для пользоваться. В теле запроса мы ожидаем получить JSON в котором будет почта и пароль пользователя. В продакшене логично JWT выдавать также и при регистрации пользователя.
router.post('/login', async(ctx, next) => {
await passport.authenticate('local', function (err, user) {
if (user == false) {
ctx.body = "Login failed";
} else {
//--payload - информация которую мы храним в токене и можем из него получать
const payload = {
id: user.id,
displayName: user.displayName,
email: user.email
};
const token = jwt.sign(payload, jwtsecret); //здесь создается JWT
ctx.body = {user: user.displayName, token: 'JWT ' + token};
}
})(ctx, next);
});
GET запрос на /custom проверяет наличие валидного JWT.
router.get('/custom', async(ctx, next) => {
await passport.authenticate('jwt', function (err, user) {
if (user) {
ctx.body = "hello " + user.displayName;
} else {
ctx.body = "No such user";
console.log("err", err)
}
} )(ctx, next)
});
Теперь сделаем финальный аккорд по настройке авторизации для socket.io. Проблема тут в том, что протокол WebSockets работает поверх tcp, а не http и механизмы REST API к нему не применимы. К счастью, для него есть модуль socketio-jwt, который позволяет достаточно лаконично описать авторизацию через JWT.
let io = socketIO(server);
io.on('connection', socketioJwt.authorize({
secret: jwtsecret,
timeout: 15000
})).on('authenticated', function (socket) {
console.log('Это мое имя из токена: ' + socket.decoded_token.displayName);
socket.on("clientEvent", (data) => {
console.log(data);
})
});
Более подробно про авторизацию через JWT для socket.io можно почитать тут [8].
Используя код выше вы можете построить рабочее Node.js приложение, используя современный подход к авторизации. Разумеетсяя в продакшене надо будет добавить ряд проверок, которые обычно стандартны для такого рода приложений.
Полную версию кода с описание того, как его протестировать можно посмотреть в GitHub. [9]
Автор: slavaLu
Источник [10]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/249635
Ссылки в тексте:
[1] Koa : http://koajs.com/
[2] тут.: https://jwt.io/introduction/
[3] Passport.js: http://passportjs.org/
[4] socket.io.: https://github.com/socketio/socket.io
[5] доке: https://nodejs.org/api/crypto.html#crypto_crypto_pbkdf2sync_password_salt_iterations_keylen_digest
[6] тут: https://github.com/jaredhanson/passport-local
[7] тут: https://github.com/themikenicholson/passport-jwt
[8] тут: https://auth0.com/blog/auth-with-socket-io/
[9] GitHub.: https://github.com/waubau/koa_jwt_auth
[10] Источник: https://habrahabr.ru/post/324066/?utm_source=habrahabr&utm_medium=rss&utm_campaign=sandbox
Нажмите здесь для печати.