javascript

Разделить пользователей по ролям в FeathersJs

  • понедельник, 8 января 2018 г. в 03:12:43
https://habrahabr.ru/post/346170/
  • JavaScript


image
Речь пойдет о недооцененном фреймворке 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-х групп пользователей