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

Теперь, когда мы понимаем, как работает Passport, бовайте создадим нашу локальную стратегию Для нашего приложения мы задействуем две локальные стратегии, одну для регистрации нового пользователя (signup) и одну для входа уже существующего (signin). У нас уже есть маршруты, использующие каждую из них, и теперь нужно сделать так, чтобы обе стратегии соответствовали каждому из них. Перед построением стратегий создадим несколько вспомогательных функций для сохранения данных в базе Orchestrate и извлечения из нее, а также файл для хранения токенов. Начнем с того, что раскомментируем нужные строки в файле index.js:
Содадим файл config.js, в котором будет храниться ключ Orchestrate API key:
Функция localAuth используется для проверки соответствия данных пользователя тем, что хранятся в базе данных. Она ищет в базе данных запись по заданному ключу username. Если таковая найдена, извлеченный из базы данных пароль сверяется с предоставленным в запросе. Если проверка пароля не прошла, запрос будет отклонен с возвратом ложного значения. Если пароль введен правильно, вернется объект user, который будет передан стратегии для верификации.
Теперь, написав вспомогательные функции, давайте создадим, наконец, нашу локальную стратегию! В секции модуля Passport файла index.js добавим:
Начинаем разработку
Давайте сначала сделаем обзор технологий, которые мы собираемся использовать:- приложение, написанное для среды 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() перенаправит запрос на соответствующую страницу. Процесс выглядит так:
Теперь, когда мы понимаем, как работает 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” увидите нового пользователя. Подравляю! Вы освоили локальную аторизацию!
Комментариев нет:
Отправить комментарий