среда, 9 ноября 2016 г.

Аутентификация пользователей с помощью Node.js, Express, Passport, и Orchestrate




via
Одним из самых заковыристых моментов во время разработки моего первого приложения оказалась реализация аутентификации пользователей. Часов, которые я провел за клавиатурой в попытках заставить эту систему работать, вместо того, чтобы радостно совершать логины и логауты, уже не вернешь. Так позвольте же мне помочь вам проскочить через все эти хитрые ловушки! В этой статье мы попробуем построить простое приложение с целью исследовать реализацию аутентификации пользователей с помощью базы данных Orchestrate и Node.js-приложения на базе сервера Express в сочетании с модулем  Passport. Мы организуем локальную аутентификацию, а также аутентификацию через Twitter, Google и Facebook. Я покажу как создавать User Authentication Strategy, как называет их Passport, применительно к каждому из перечисленных четырех методов. Первый раздел показывает, как производить установку нашего приложения и аутентифицировать локальных пользователей.

Начинаем разработку

Давайте сначала сделаем обзор технологий, которые мы собираемся использовать:
  • приложение, написанное для среды Node.js
  • запускается на сервере Express
  • с модулем Passport для аутентификации
  • базой данных Orchestrate для хранения и получения информации о пользователях 
  • Twitter Bootstrap - для организации красивого внешнего вида
  • Handlebars - для шаблонов
В этом разделе мы:
  • соберем наше базовое приложение
  • соберем шаблоны внешнего вида
  • настроим маршрутизацию
  • сконфигурируем Passport для локальной регистрации и входа
  • протестируем регистрацию и логин
  • дружески похлопаем себя по спине и расскажем друзьям о том, какие мы крутые.

Базовое приложение

Для начала нам нужно установить базовое приложение. Если у вас уже установлены среда Node.js и пакетный менеджер npm, тогда вперед! Если нет - отправляйтесь на сайт Node.js. там вы найдете всё необходимое. Теперь давайте установим необходимые зависимости и пакеты. Создайте файл package.json вручную или скачайте его в составе репозитория, содержащего материалы данной стать, с  GitHub (ссылка приводится в конце текста). Убедитесь, что в вашем файле package.json присутствуют такие строки:
 
//package.json
{
 "name": "userAuth",
 "main": "index.js",
 "dependencies": {
   "bcryptjs": "^0.7.12",
   "express": "^4.9.5",
   "express-handlebars": "^1.1.0",
   "morgan": "^1.3.2",
   "body-parser": "^1.9.0",
   "cookie-parser": "^1.3.3",
   "method-override": "^2.2.0",
   "express-session": "^1.8.2",
   "orchestrate": "^0.3.11",
   "passport": "^0.2.1",
   "passport-facebook": "^1.0.3",
   "passport-google": "^0.3.0",
   "passport-local": "^1.0.0",
   "passport-twitter": "^1.0.2",
   "q": "^1.0.1"
 }
}
 
Создав и проверив файл, запустите команду:
 
$ npm install
 
Теперь, когда установлены необходимые зависимости, самое время создать наш сервер. (Обратите внимание: в данном разделе будут задействованы не все эти зависимости, и это нормально, поскольку некоторые из них понадобятся нам в будущем). Давайте пробежимся по файлу index.js, в котором описан наш сервер, и задействуем необходимые пакеты:
 
//index.js/
var express = require('express'),
    exphbs = require('express-handlebars'),
    logger = require('morgan'),
    cookieParser = require('cookie-parser'),
    bodyParser = require('body-parser'),
    methodOverride = require('method-override'),
    session = require('express-session'),
    passport = require('passport'),
    LocalStrategy = require('passport-local'),
    TwitterStrategy = require('passport-twitter'),
    GoogleStrategy = require('passport-google'),
    FacebookStrategy = require('passport-facebook');

//Эти файлы пока не нужны, их создадим чуть позже:
// var config = require('./config.js'), //config file contains all tokens and other private info
//    funct = require('./functions.js'); //funct file contains our helper functions for our Passport and database work

var app = express();

//===============PASSPORT===============

//Здесь будет всё, что касается модуля Passport.

//===============EXPRESS================
// Сконфигурируем Express
app.use(logger('combined'));
app.use(cookieParser());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.use(methodOverride('X-HTTP-Method-Override'));
app.use(session({secret: 'supernova', saveUninitialized: true, resave: true}));
app.use(passport.initialize());
app.use(passport.session());

// Установим модуль поддержки "вечных" сессий:
app.use(function(req, res, next){
  var err = req.session.error,
      msg = req.session.notice,
      success = req.session.success;

  delete req.session.error;
  delete req.session.success;
  delete req.session.notice;

  if (err) res.locals.error = err;
  if (msg) res.locals.notice = msg;
  if (success) res.locals.success = success;

  next();
});

// Сконфигурируем Express lkz hf,jns с шаблонами handlebars:
var hbs = exphbs.create({
    defaultLayout: 'main', // вскоре займемся этим шаблоном
});
app.engine('handlebars', hbs.engine);
app.set('view engine', 'handlebars');

//===============ROUTES===============

// Здесь будут маршруты.

//===============PORT=================
var port = process.env.PORT || 5000; //select your port or let it pull from your .env file
app.listen(port);
console.log("listening on " + port + "!");
 
Всё готово к тестированию нашего сервера:
 
$ node index.js
 
Если всё настроено без ошибок, мы должны увидеть сообщение со строкой “listening on port 5000!” или другим портом, который вы задали при запуске приложения. Двинемся дальше и остановим сервер, настало время создать шаблоны, чтобы приложение обрело приятный внешний вид.
Если вы создаете файлы вручную, а не клонировали их из репозитория, создайте папку views в каталоге вашего проекта. В папке  views будут находиться два файла (home.handlebars и signin.handlebars) и одна подпапка (layouts). Подпапка layouts будет содержать один файл (main.handlebars), который отвечает за внешний вид по умолчанию нашего приложения. Файл main.handlebars выглядит так:
 
<!-- views/layouts/main.handlebars -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="description" content="User Authentication">
  <meta name="author" content="">

  <title>User Authentication</title>

  <!-- Latest compiled and minified CSS -->
  <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css">

  </head>

  <body>

    <div class="container">

      <nav class="navbar navbar-default" role="navigation">
      <div class="container-fluid">

      <!-- Brand and toggle get grouped for better mobile display -->
      <div class="navbar-header">
        <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
          <span class="sr-only">Toggle navigation</span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
        </button>
      </div>

        <!-- Collect the nav links, forms, and other content for toggling -->
        <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
          <ul class="nav navbar-nav">
            <li>
              {{#if user}}
                <p class="navbar-text">
                  <strong>Hi,</strong>
                  <img src="{{user.avatar}}" width="20" height="20">
                  {{user.username}}
                </p>
                </li>
                </ul>
                <ul class="nav navbar-nav navbar-right">
                  <li>
                    <a href="/logout">Log Out</a>
                  </li>
              {{else}}
                <a href="/signin">Sign In</a>
                </li>
              {{/if}}
          </ul>
        </div><!-- /.navbar-collapse -->
      </div><!-- /.container-fluid -->
    </nav>

    {{#if error}}
      <p class="alert alert-warning">{{error}}</p>
    {{/if}}

    {{#if success}}
      <p class="alert alert-success">{{success}}</p>
    {{/if}}

    {{#if notice}}
      <p class="alert alert-info">{{notice}}</p>
    {{/if}}

    <!--where our other templates will insert-->
    {{{body}}}

    </div> <!-- /container -->

    <!-- Bootstrap core JavaScript
    ================================================== -->
    <!-- Placed at the end of the document so the pages load faster -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>

    <!-- Latest compiled and minified JavaScript -->
  <script src="//netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js"></script>
</body>
</html>
 
Вот содержимое файла home.handlebars:
 
<!-- views/home.handlebars -->
{{#if user}}
  <div class="jumbotron">
    <h1>Orchestrate User Authentication</h1>
    <p>Below is your profile information!</p>
  </div>
  <div class="panel panel-default">
    <div class="panel-heading">
      <h3 class="panel-title">Profile Information</h3>
    </div>
    <div class="panel-body">
      <p>Username: {{user.username}}</p>
      <p>Avatar: <img src="{{user.avatar}}"/></p>
    </div>
  </div>

{{else}}
  <div class="jumbotron">
    <h1>Orchestrate User Authentication</h1>
    <p>Sign in and view your profile!</p>
    <p>
      <a href="/signin" class="btn btn-primary btn-lg" role="button">
        <span class="glyphicon glyphicon-user"></span>
        Sign in!
      </a>
    </p>
  </div>

{{/if}}
 
Наконец, наш signin.handlebars:

<!-- views/signin.handlebars -->
<div class="jumbotron">
  <h1>Sign in</h1>
  <p>We're using passport.js to demonstrate user authentication. Please sign-in with your Local, Twitter, Google, or Facebook account to view your profile.</p>
  <p>
    <a data-toggle="collapse" href="#local" class="btn btn-warning btn-lg" role="button"><span class="glyphicon glyphicon-user"></span> Sign in Locally</a>
    <a href="/auth/twitter" class="btn btn-info btn-lg" role="button"><span class="glyphicon glyphicon-user"></span> Sign in with Twitter</a>
    <a href="/auth/google" class="btn btn-danger btn-lg" role="button"><span class="glyphicon glyphicon-user"></span> Sign in with Google</a>
    <a href="/auth/facebook" class="btn btn-primary btn-lg" role="button"><span class="glyphicon glyphicon-user"></span> Sign in with Facebook</a>
  </p>

  <div id="local" class="collapse">
    <a data-toggle="collapse" href="#local-sign-in" class="btn btn-default btn-md" role="button">I already have an account</a>
    <a data-toggle="collapse" href="#local-reg" class="btn btn-default btn-md" role="button">I need to make an account</a>
  </div>

  <form id="local-sign-in" class="collapse" action="/login" method="post">
    <div>
        <p></p>
        <label>Username:</label>
        <input type="text" name="username"/>
    </div>
    <div>
        <label>Password:</label>
        <input type="password" name="password"/>
    </div>
    <div>
        <input type="submit" class="btn btn-primary btn-sm" value="Log In"/>
    </div>
  </form>

  <form id="local-reg" class="collapse" action="/local-reg" method="post">
    <div>
        <p></p>
        <label>New Username:</label>
        <input type="text" name="username"/>
    </div>
    <div>
        <label>New Password:</label>
        <input type="password" name="password"/>
    </div>
    <div>
        <input type="submit" class="btn btn-primary btn-sm" value="Register"/>
    </div>
  </form>
</div>

Маршруты

Теперь, когда мы разобрались с внешним видом, авайте пропишем маршруты, чтобы мы реально смогли увидеть наши страницы. В секции routes файла index.js добавим следующие строки:
 
//===============ROUTES=================
//displays our homepage
app.get('/', function(req, res){
  res.render('home', {user: req.user});
});

//displays our signup page
app.get('/signin', function(req, res){
  res.render('signin');
});

//sends the request through our local signup strategy, and if successful takes user to homepage, otherwise returns then to signin page
app.post('/local-reg', passport.authenticate('local-signup', {
  successRedirect: '/',
  failureRedirect: '/signin'
  })
);

//sends the request through our local login/signin strategy, and if successful takes user to homepage, otherwise returns then to signin page
app.post('/login', passport.authenticate('local-signin', {
  successRedirect: '/',
  failureRedirect: '/signin'
  })
);

//logs user out of site, deleting them from the session, and returns to homepage
app.get('/logout', function(req, res){
  var name = req.user.username;
  console.log("LOGGIN OUT " + req.user.username)
  req.logout();
  res.redirect('/');
  req.session.notice = "You have successfully been logged out " + name + "!";
});
 
Хотя мы еще и не настроили стратегии модуля Passport, мы уже можем видеть домашнюю страницу и осуществлять вход с помощью браузера. Пока всё идет хорошо.

Модуль Passport

Говоря о стратегиях модуля Passport, давайте сначала рассмотрим как он вообще работает. Согласно документации, различные способы аутентификации, называемые стратегиями, могут быть установлены в виде модулей. В этй статье мы задействуем так называемую локальную стратегию. Toon Ketels хорошо описывает последовательность работы модуля Passport в своем блоге. Важно здесь то, что когда пользователь вводит свои учетные данные, запрос попадает в функцию authenticate() модуля Passport, и задействует ту стратегию, которую мы сконфигурировали для данного маршрута. Эти учетные данные (req.body.username и req.body.password) попадают "внутрь" стратегии и подвергаются верификации. Если она прошла успешно, данные пользователя будут преобразованы в идентификатор сессии и произойдет взод в приложение, если нет - стратегия вернет сообщение об ошибкеи функция authenticate() перенаправит запрос на соответствующую страницу. Процесс выглядит так:



orchestrate-passport-strategy



Теперь, когда мы понимаем, как работает Passport, бовайте создадим нашу локальную стратегию Для нашего приложения мы задействуем две локальные стратегии, одну для регистрации нового пользователя (signup) и одну для входа уже существующего (signin). У нас уже есть маршруты, использующие каждую из них, и теперь нужно сделать так, чтобы обе стратегии соответствовали каждому из них. Перед построением стратегий создадим несколько вспомогательных функций для сохранения данных в базе Orchestrate и извлечения из нее, а также файл для хранения токенов. Начнем с того, что раскомментируем нужные строки в файле index.js:
 
// index.js/

var config = require('./config.js'), //config file contains all tokens and other private info
    funct = require('./functions.js'); //funct file contains our helper functions for our Passport and database work

Содадим файл config.js, в котором будет храниться ключ Orchestrate API key:
 
// config.js/
module.exports = {
  "db": “YOUR_ORCHESTRATE_API_KEY”
}
 
Теперь подключаем файл functions.js, содержащий полезные функции:
 
// functions.js/
var bcrypt = require('bcryptjs'),
    Q = require('q'),
    config = require('./config.js'), //config file contains all tokens and other private info
    db = require('orchestrate')(config.db); //config.db holds Orchestrate token

//used in local-signup strategy
exports.localReg = function (username, password) {
  var deferred = Q.defer();
  var hash = bcrypt.hashSync(password, 8);
  var user = {
    "username": username,
    "password": hash,
    "avatar": "http://placepuppy.it/images/homepage/Beagle_puppy_6_weeks.JPG"
  }
  //check if username is already assigned in our database
  db.get('local-users', username)
  .then(function (result){ //case in which user already exists in db
    console.log('username already exists');
    deferred.resolve(false); //username already exists
  })
  .fail(function (result) {//case in which user does not already exist in db
      console.log(result.body);
      if (result.body.message == 'The requested items could not be found.'){
        console.log('Username is free for use');
        db.put('local-users', username, user)
        .then(function () {
          console.log("USER: " + user);
          deferred.resolve(user);
        })
        .fail(function (err) {
          console.log("PUT FAIL:" + err.body);
          deferred.reject(new Error(err.body));
        });
      } else {
        deferred.reject(new Error(result.body));
      }
  });

  return deferred.promise;
};

//check if user exists
    //if user exists check if passwords match (use bcrypt.compareSync(password, hash); // true where 'hash' is password in DB)
      //if password matches take into website
  //if user doesn't exist or password doesn't match tell them it failed
exports.localAuth = function (username, password) {
  var deferred = Q.defer();

  db.get('local-users', username)
  .then(function (result){
    console.log("FOUND USER");
    var hash = result.body.password;
    console.log(hash);
    console.log(bcrypt.compareSync(password, hash));
    if (bcrypt.compareSync(password, hash)) {
      deferred.resolve(result.body);
    } else {
      console.log("PASSWORDS NOT MATCH");
      deferred.resolve(false);
    }
  }).fail(function (err){
    if (err.body.message == 'The requested items could not be found.'){
          console.log("COULD NOT FIND USER IN DB FOR SIGNIN");
          deferred.resolve(false);
    } else {
      deferred.reject(new Error(err));
    }
  });

  return deferred.promise;
}
 
Функция localReg будет использоваться в нашей локальной стратегии для регистрации нового пользователя и созранения данных о нем в базе данных. Она проверяет объект user, шифруя при этом пароль. Затем функция проверяет, не суествует ли уже такой пользователь в базе данных. Если да - запрос отклоняется и возвращается ложное значение, что предотвращает повторную регистрацию пользователей. Если регистрирующийся пользователь не существует, объект user, созданный нами, будет сохранен с использованием поля username как ключа. Затем этот объект будет озвращен, чтобы стратегия могда произвести его верификацию.

Функция  localAuth используется для проверки соответствия данных пользователя тем, что хранятся в базе данных. Она ищет в базе данных запись по заданному ключу username. Если таковая найдена, извлеченный из базы данных пароль сверяется с предоставленным в запросе. Если проверка пароля не прошла, запрос будет отклонен с возвратом ложного значения. Если пароль введен правильно, вернется объект user, который будет передан стратегии для верификации.
Теперь, написав вспомогательные функции, давайте создадим, наконец, нашу локальную стратегию! В секции модуля Passport файла index.js добавим:
 
// index.js/
//===============PASSPORT=================
// Use the LocalStrategy within Passport to login/”signin” users.
passport.use('local-signin', new LocalStrategy(
  {passReqToCallback : true}, //allows us to pass back the request to the callback
  function(req, username, password, done) {
    funct.localAuth(username, password)
    .then(function (user) {
      if (user) {
        console.log("LOGGED IN AS: " + user.username);
        req.session.success = 'You are successfully logged in ' + user.username + '!';
        done(null, user);
      }
      if (!user) {
        console.log("COULD NOT LOG IN");
        req.session.error = 'Could not log user in. Please try again.'; //inform user could not log them in
        done(null, user);
      }
    })
    .fail(function (err){
      console.log(err.body);
    });
  }
));
// Use the LocalStrategy within Passport to register/"signup" users.
passport.use('local-signup', new LocalStrategy(
  {passReqToCallback : true}, //allows us to pass back the request to the callback
  function(req, username, password, done) {
    funct.localReg(username, password)
    .then(function (user) {
      if (user) {
        console.log("REGISTERED: " + user.username);
        req.session.success = 'You are successfully registered and logged in ' + user.username + '!';
        done(null, user);
      }
      if (!user) {
        console.log("COULD NOT REGISTER");
        req.session.error = 'That username is already in use, please try a different one.'; //inform user could not log them in
        done(null, user);
      }
    })
    .fail(function (err){
      console.log(err.body);
    });
  }
));
 
Последний фрагмент секции Passport - сериализация и десериализация пользователя из данныхсессии. Поскольку на объект user очень простой, мы сериализируем и десериализируем его полностью, по по мере того, как он будет разростаться и усложняться, мы будем выбирать из него какой-нибудь один аспект. Для наших целей достаточно вставить в секцию  Passport следующее:
 
// index.js/
//===============PASSPORT=================
// Passport session setup.
passport.serializeUser(function(user, done) {
  console.log("serializing " + user.username);
  done(null, user);
});

passport.deserializeUser(function(obj, done) {
  console.log("deserializing " + obj);
  done(null, obj);
});
 
Следует заметить, что если в вашем приложении есть зоны, которые вы хотите защитить так, чтобы только авторизованные пользователи имели доступ к ним, Passport предоставляет простую возможность для этого. Просто используйте следующую функцию в ваших маршрутах к защищенным секциям:
 
// Simple route middleware to ensure user is authenticated.
function ensureAuthenticated(req, res, next) {
  if (req.isAuthenticated()) { return next(); }
  req.session.error = 'Please sign in!';
  res.redirect('/signin');
}
 
Теперь у нас есть приложение с работающей локальной аутентификацией! Испытаем ее. Если мы запустим сервер и направим браузер на нужный  порт, мы увидим большую синюю кнопку, кликнув по которой вызовем меню  “Sign in”. Отсюда пункт “Sign in Locally” предоставит возможность либо войти с существующей записью, либо создать новую. Поскольку у нас еще нет пользователей в базе данных, нам нужно будет создать новую. Вы можете проверить безопасность, попробовав войти в несуществующий аккаунт, что, как увидите, запрещено. Создав учетную запись, вы должны получить возможность войти на сайт.  Если вы заглянете в вашу базу данных Orchestrate, то в разделе “local-users” увидите нового пользователя. Подравляю! Вы освоили локальную аторизацию!

Комментариев нет:

Отправить комментарий