А так же о всякой фигне
Авторизацией в hapi занимаются отдельные модули, в нпм репозитории их куча, на любой вкус. Мне приходилось работать только с bearer токенами, про них я и расскажу.
Для начала нужно установить модули:
npm i hapi-auth-bearer-token crypto
Нам нужна будет табличка, в которой будем хранить токены, добавляем новую модель:
'use strict'; const Crypto = require('crypto'); module.exports = (sequelize, DataTypes) => { const accessToken = sequelize.define('access_tokens', { id: { allowNull: false, autoIncrement: true, primaryKey: true, type: DataTypes.INTEGER }, token: { type: DataTypes.STRING, allowNull: false }, user_id: { type: DataTypes.INTEGER, allowNull: false }, expires_at: { type: DataTypes.DATE, allowNull: false }, }); accessToken.generateAccessTokenString = () => { // eslint-disable-next-line no-undef return Crypto.createHmac('md5', Crypto.randomBytes(512).toString()).update([].slice.call(arguments).join(':')).digest('hex'); }; accessToken.createAccessToken = (user) => { const options = { user_id: user.get('id'), expires_at: (new Date(new Date().valueOf() + (30 * 24 * 60 * 60 * 1000))), access_token: accessToken.generateAccessTokenString(user.get('id'), user.get('email'), new Date().valueOf()) }; return accessToken.create(options); }; accessToken.dummyData = [ { id: 1, token: 'a47fa9fead309305dddf17bdde0d75b8', user_id: 1, expires_at: new Date() + 1000*60*60*24*7 // 1 week } ]; return accessToken; };
Затем нужно зарегистрировать модуль в нашем сервере:
... const AuthBearer = require('hapi-auth-bearer-token'); ... await server.register([ ... AuthBearer, hapiBoomDecorators, Inert, ... ]);
После, нужно добавить стратегию авторизации
server.auth.strategy('token', 'bearer-access-token', { // Создаём новую стратегию с именем 'token' allowQueryToken: false, unauthorized: bearerValidation.unauthorized, // вешаем функцию-обработчик не авторизованных запросов validate: bearerValidation.validate // а вот тут будем решать авторизирован запрос или нет });
Ну а теперь нужно написать сами обработчики, наш новый модуль bearerValidation:
// ./src/libs/bearerValidation.js const Boom = require('boom'); const Op = require('sequelize').Op; async function unauthorized () { // Пока эта функция ничего не делает, только возвращает стандартный ответ throw Boom.unauthorized(); // Но сюда можно добавить много чего интересного } async function validate (request, token) { // а вот тут уже начинаем проверку запроса // в request лежит всё то же самое что и в обычном руте, включая модели // а в token - сам наш токен, который прислал клиент const accessToken = request.getModel(request.server.config.db.database, 'access_tokens'); const users = request.getModel(request.server.config.db.database, 'users'); // Херачим запрос в бд, и смотрим есть ли такой токен и валиден ли он let dbToken = await accessToken.findOne({ where: { token: token, expires_at: { [ Op.gte ]: new Date() } } }); if( !dbToken ) { // Нет такого токена, либо он просрочен return { isValid: false, credentials: {} }; } // Ищем юзера let curUser = await users.findOne({ where: { id: dbToken.dataValues.user_id } }); if( !curUser ) {// Если юзера нет, то говорим, что неавторизованы // Нужно удалить невалидный токен accessToken.destroy({ where: { user_id: dbToken.dataValues.user_id } }); return { isValid: false, credentials: {} }; } // Если же юзер есть, то return { isValid: true, credentials: { role: 'admin' // Сюда фигачим роль юзера }, artifacts: { // а вот сюда фигачим любые данные которые нам могут пригодиться внутри рута token: token, user: curUser.dataValues } }; } module.exports = { validate: validate, unauthorized: unauthorized };
Ну и теперь нам нужно написать пару роутов чтобы это всё заработало.
Авторизация:
//./src/routes/auth/post.js const Joi = require('joi'); const Boom = require('boom'); async function response(request) { // Подключаем модельки const accessTokens = request.getModel(request.server.config.db.database, 'access_tokens'); const users = request.getModel(request.server.config.db.database, 'users'); // Ищем пользователя по мылу let userRecord = await users.findOne({ where: { email: request.query.login } }); // если не нашли, говорим что не авторизованы if( !userRecord ) { throw Boom.unauthorized(); } // Проверяем совподают ли пароли if( !userRecord.verifyPassword(request.query.password) ) { throw Boom.unauthorized();// если нет, то опять ж говорим, что не авторизованы } // Иначе, создаём новый токен let token = await accessTokens.createAccessToken(userRecord); // и возвращаем его return { meta: { total: 1 }, data: [token.dataValues] }; } // А тут описываем схему ответа const tokenScheme = Joi.object({ id: Joi.number().integer().example(1), user_id: Joi.number().integer().example(2), expires_at: Joi.date().example('2019-02-16T15:38:48.243Z'), token: Joi.string().example('4443655c28b42a4349809accb3f5bc71'), updatedAt: Joi.date().example('2019-02-16T15:38:48.243Z'), createdAt: Joi.date().example('2019-02-16T15:38:48.243Z') }); const responseScheme = Joi.object({ meta: Joi.object({ total: Joi.number().integer().example(3) }), data: Joi.array().items(tokenScheme) }); module.exports = { method: 'GET', path: '/auth', options: { handler: response, tags: ['api'], // Necessary tag for swagger validate: { query: { login: Joi.string().required(), password: Joi.string().required() } }, response: { schema: responseScheme } // если схема ответа не будет совподать с тем что реально отдаётся // сервер отдаст 500 ошибку } };
И теперь, в любом нашем руте, всего лишь нужно добавить тег: "auth: 'token'", чтобы он стал доступен только по авторизации. Например переделаем POST /messages
// ./src/routes/mesages/post.js const Joi = require('joi'); async function response(request) { const messages = request.getModel(request.server.config.db.database, 'messages'); let newMessage = await messages.create(request.payload); let count = await messages.count(); return { meta: { total: count }, data: [ newMessage ] }; } const messageSchema = Joi.object({ id: Joi.number().integer().example(1), user_id: Joi.number().integer().example(2), message: Joi.string().example('Lorem ipsum') }); const responseScheme = Joi.object({ meta: Joi.object({ total: Joi.number().integer().example(3) }), data: Joi.array().items(messageSchema) }); module.exports = { method: 'POST', path: '/messages', options: { handler: response, tags: ['api'], // Necessary tag for swagger auth: 'token', // >>>> Это необходимый тег для включения авторизации validate: { payload: { user_id: Joi.number().integer().required().example(1), message: Joi.string().min(1).max(100).required().example('Lorem ipsum') } }, response: { schema: responseScheme } } };
Проверяем:
user@Thinik:~$ curl -X POST --header 'Content-Type: application/json' --header 'Accept: application/json' -d 'ololo ololo' 'http://localhost:3030/messages' {"statusCode":401,"error":"Unauthorized","message":"Unauthorized"}
Всё ок, доступ без токена закрыт, теперьь то же самое но с токеном:
user@Thinik:~$ curl -X GET --header 'Accept: application/json' 'http://localhost:3030/auth?login=pupkin%40gmail.com&password=12345' {"meta":{"total":1},"data":[{"id":8,"user_id":1,"expires_at":"2019-02-16T15:54:42.521Z","token":"669979fad3f109282177c6fc8896fed8","updatedAt":"2019-01-17T15:54:42.523Z","createdAt":"2019-01-17T15:54:42.523Z"}]} // копипастим "token":"669979fad3f109282177c6fc8896fed8" в следующий запрос user@Thinik:~$ curl -X POST --header 'Content-Type: application/json' --header 'Accept: application/json' 'http://localhost:3030/messages' -H 'Authorization: Bearer 669979fad3f109282177c6fc8896fed8' --data '{"user_id": 1, "message": "olololo"}' {"meta":{"total":9},"data":[{"id":9,"user_id":1,"message":"olololo","updatedAt":"2019-01-17T16:02:24.875Z","createdAt":"2019-01-17T16:02:24.875Z"}]}
Работает)
Как обычно, все исходники на гитхабе: https://github.com/hololoev/api_hapi_example_5
Т.к. беда не приходит одна, купили мы бук ещё и для жены. И естестно для девочки бук должен быть няшненький.
Как использовать ORM Sequelize
Sequelize - это ORM библиотека для nodejs. Sequelize поддерживает PostgreSQL, MySQL, SQLite и MSSQL диалекты.