
Речь пойдет о недооцененном фреймворке 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 на бэке, а осуществляется переход по ссылке, то есть на фронте будет это выглядеть так:
<a href="/auth/facebook">Войти через Fb</a>
Если в двух словах, то после нажатия на ссылку произойдет редирект на бэк, бэк средиректит на 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-х групп пользователей
