Разделить пользователей по ролям в FeathersJs
Речь пойдет о недооцененном фреймворке feathersjs.
В двух словах как он работает вы можете почитать тут.
Одним рабочим утром мне в месседжере пришло новое ТЗ и описано оно было следующим образом: нужно разделить пользователей для 2-х сервисов (то есть что бы бэк обрабатывал запросы на аутентификацию с двух разных фронтов).
То есть мы имеем: 2 разделенных фронта написанных на VueJs, находящихся на разных доменах именах, общий backend написанный на feathers (и естественно одну таблицу users в бд с ролью).
То есть таблица может содержать 2 вот таких поля
email pass project
+------------+--------+-------
1@gmail.com 123 front_one
1@gmail.com 123 front_two
Естественно пароль хэширован.
В качестве модуля для входа в сервис мы используем feathers-authentication (адаптированная для feathers версия passportjs).
И так, что касается local login то тут все просто. Для того что бы нам определить откуда на бэк пришел запрос с парой логин/пароль в тело запроса на фронте можно вставить еще один параметр, например «project», и по этому параметру искать в базе нужного пользователя.
Чуть подробнее как сделать local auth пользователя. Я создал отдельный файл для аунтификации (именно файл, а не генерировал сервис) auth.js, подключил его в app.js. Так auth.js примерно будет выглядить у вас
const authentication = require('feathers-authentication');
const jwt = require('feathers-authentication-jwt');
const local = require('feathers-authentication-local');
const oauth2 = require('feathers-authentication-oauth2');
const FacebookStrategy = require('passport-facebook');
const commonHooks = require('feathers-hooks-common');
module.exports = function () {
const app = this;
const config = app.get('authentication');
app.configure(authentication(config));
app.configure(jwt());
app.configure(local(config.local));
app.service('authentication').hooks({
before: {
create: [
commonHooks.lowerCase('email'),
authentication.hooks.authenticate(config.strategies)
],
remove: [
authentication.hooks.authenticate('jwt')
]
}
});
};
В общем сейчас в нем нет ничего интересного, единственное хотел бы отметить, что я не вынес hooks в отдельных файл (как это происходит, когда вы вызываете стандартный generate service), и добавил hook commonHooks.lowerCase ('email') — думаю понятно для чего он нужен.
А теперь добавим немного магии. Покопавшись в документации я нашел класс verifier, который можно расширить и дописать свой ф-ционал. Я добавил в анонимную ф-цию новую конфигурацию для local auth
app.configure(local({ Verifier: CustomVerifier }));
и вызвал мой новый класс
class CustomVerifier extends Verifier {
verify(req, username, password, done) {
return this.app.service('users').find({
query: {
email: username,
roles: req.query.project
}
}).then(res => {
const user = res.data[0];
if (user) {
const userId = res.data[0].id;
this._comparePassword(user, password).then(() => {
if (!user.isVerified) {
done(null)
} else {
done(null, user, { userId: userId });
}
}).catch(err => {
done(null)
})
} else {
done(null)
}
})
}
}
Что же этот класс делает? Сначала мы подключаемся к сервису users — this.app.service ('users') — и вызываем метод find с параметром query. То есть мы ищем в бд нужного нам пользователя по двум полям и если его находим то в ответе (переменная res) будет массив найденых пользователей, если пользователи не найдены, то массив вернется пустой. Потом мы вызываем ф-цию
this._comparePassword()
куда передаем в качестве параметра найденного пользователя и пароль который пришел с фронта. Ф-ция _comparePassword хэширует пароль и сравнивает его с тем паролем, который лежит в бд и если пароль совпадает то мы вызываем в then ()
done(null, user, { userId: userId });
где первый аргумент это объект ошибки, второй — текущий пользователь, третий — id пользователя в бд, done () в свою очередь возвращает корректный токен. Если передать в done () единственный аргумент null, то статус запроса станет 401, а в ответе мы получим
сlassName:"not-authenticated"
code:401
errors:{}
message:"Error"
name:"NotAuthenticated"
И на этом дело бы закончилось, но в наш сервис можно так же зайти через facebook. Для того что бы это было возможно, в анонимную ф-цию нужно добавить следующее:
app.configure(oauth2(Object.assign({
name: 'facebook',
Strategy: FacebookStrategy,
Verifier: CustomVerifierFB
}, config.facebook)));
В этом коде опять же нас интересует только один параметр: «Verifier: CustomVerifierFB». Мы, как и в случае c локальной регистрацией, расширяем встроенный класс Verifier. При login через fb на фронте не отправляется запрос на определенный URL на бэке, а осуществляется переход по ссылке, то есть на фронте будет это выглядеть так:
Войти через Fb
Если в двух словах, то после нажатия на ссылку произойдет редирект на бэк, бэк средиректит на FB, FB средиректит на бэк, бэк запишет в cookies сгенерированный токен, и отправит на главную страницу фронта. На фронте же вам нужно распарсить cookies и следующие запросы отправлять на бэк уже с новым токеном.
И не было бы этой статьи, но не мало времени я потратил на вопрос — А как же собственно узнать откуда пришел пользователь?
Ответ оказался достаточно простым. Перед регистрацией компонента для входа через FB нужно сделать так:
app.get('/auth/facebook', (req, res, next) => {
referOrigin = req.headers.referer
next();
})
app.configure(oauth2(Object.assign({
name: 'facebook',
Strategy: FacebookStrategy,
Verifier: CustomVerifierFB
}, config.facebook)));
То есть мы отлавливаем переход на '/auth/facebook', и записываем в глобальную переменную (referOrigin) значение req.headers.referer и запускаем регистрацию oauth2(). Таким образом мы получаем значение хоста, в глобальной переменной и можем использовать это значение в классе CustomVerifierFB, который будет выглядеть примерно так:
class CustomVerifierFB extends Verifier {
verify(req, accessToken, refreshToken, profile, done) {
const refer = referOrigin
let roles = ''
if (refer === 'front_one') {
roles = 'front_one'
} else {
roles = 'front_two'
}
return this.app.service('users').find({
query: {
facebookId: profile.id,
roles: roles
}
}).then(res => {
if (res.data[0]) {
done(null, res.data[0], { userId: res.data[0].id });
} else {
return this.app.service('users').create({
facebookId: profile.id,
email: profile._json.email,
first_name: profile._json.first_name,
last_name: profile._json.last_name,
gender: profile._json.gender,
avatar: profile._json.picture.data.url,
roles: roles,
isVerified: true,
username: profile._json.email + 'whereFromUser'
}).then(createRes => {
done(null, createRes, { userId: createRes.id });
})
}
})
}
}
В ф-ции verify мы проделали следующее:
- this.app.service ('users').find () — ищем, есть ли в базе пользова с facebookId, который пришел нам в качестве ответа с FB
- done (null, res.data[0], { userId: res.data[0].id }) — если есть, то создаем новый токен и возвращаем его на фронт
- this.app.service ('users').create () если не нашли, то создаем такого пользователя и потом вы зываем done ()
Вот так я решил задачу по разделению пользователей для двух разных фронтов.
PS — потом напишу как я сделал восстановление паролей для 2-х групп пользователей