Аутентификация между серверов на node js. Маршруты Index и Users. Что такое Redis
Так же как аутентификация важна для API (* Application Programming Interface - программный интерфейс приложения. Здесь и далее примеч. пер.), она является важной особенностью определенных веб-приложений - тех, у которых имеются страницы и секреты, к которым должен быть доступ только у пользователей, прошедших регистрацию и аутентификацию.
В данном руководстве в процессе изучения реализации регистрации пользователей вы создадите веб-приложение.
Установка приложенияСоздайте новую папку, в которой вы будете работать. В соответствии с темой данного руководства я назвал мою site-auth . Инициализируйте проект в только что созданной папке. Ниже показано, как вы можете это сделать.
Npm init -y
Флаг -y сообщает npm (* менеджер пакетов для JavaScript) о необходимости использования опций по умолчанию.
Отредактируйте часть файла package.json так, чтобы она выглядела, как моя.
#package.json { "name": "site-auth", "version": "1.0.0", "description": "", "main": "app.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": , "author": "izuchukwu1", "license": "ISC", "dependencies": { "bcryptjs": "^2.4.3", "body-parser": "^1.17.1", "connect-flash": "^0.1.1", "cookie-parser": "^1.4.3", "express": "^4.15.2", "express-handlebars": "^3.0.0", "express-messages": "^1.0.1", "express-session": "^1.15.2", "joi": "^13.0.1", "mongoose": "^4.11.12", "morgan": "^1.8.1", "passport": "^0.4.0", "passport-local": "^1.0.0" } }
После этого запустите команду для установки пакетов.
Npm install
Создайте файл в вашей рабочей папке под названием app.js.
Начните с запрашивания зависимостей, которые вы установили, и необходимых файлов.
#app.js const express = require("express"); const morgan = require("morgan") const path = require("path"); const cookieParser = require("cookie-parser"); const bodyParser = require("body-parser"); const expressHandlebars = require("express-handlebars"); const flash = require("connect-flash"); const session = require("express-session"); const mongoose = require("mongoose") const passport = require("passport") require("./config/passport")
Эти зависимости были установлены после выполнения команды npm install. Для того чтобы их использовать в вашем приложении вам необходимо запросить и сохранить их в соответствующие константы.
В данном руководстве вы будете использовать в качестве базы данных MongoDB. Вам необходимо будет сохранить информацию о пользователе в базу данных. Для работы с MongoDB вы будете использовать Mongoose - инструмент для моделирования в Node.js (* ODM (Object Document Mapper - объектно-документный отобразитель)).Настройка Mongoose не вызывает трудностей. Выполняется следующим образом:
#app.js mongoose.Promise = global.Promise mongoose.connect("mongodb://localhost:27017/site-auth")
На данном этапе давайте настроим наше промежуточное ПО (* программное обеспечение).
// 1 const app = express() app.use(morgan("dev")) // 2 app.set("views", path.join(__dirname, "views")) app.engine("handlebars", expressHandlebars({ defaultLayout: "layout" })) app.set("view engine", "handlebars") // 3 app.use(bodyParser.json()) app.use(bodyParser.urlencoded({ extended: false })) app.use(cookieParser()) app.use(express.static(path.join(__dirname, "public"))) app.use(session({ cookie: { maxAge: 60000 }, secret: "codeworkrsecret", saveUninitialized: false, resave: false })); app.use(passport.initialize()) app.use(passport.session()) // 4 app.use(flash()) app.use((req, res, next) => { res.locals.success_mesages = req.flash("success") res.locals.error_messages = req.flash("error") next() }) // 5 app.use("/", require("./routes/index")) app.use("/users", require("./routes/users")) // 6 // catch 404 and forward to error handler app.use((req, res, next) => { res.render("notFound") }); // 7 app.listen(5000, () => console.log("Server started listening on port 5000!"))
views (* представления). Внутри нее создайте две другие папки под названиями layouts и partials . Сформируйте подобную древовидную структуру в папке views. Для этого создайте необходимые файлы в их соответствующих папках.
├── dashboard.handlebars ├── index.handlebars ├── layouts │ └── layout.handlebars ├── login.handlebars ├── notFound.handlebars ├── partials │ └── navbar.handlebars └── register.handlebars
После этого пришло время писать код.
#dashboard.handlebars User DashBoard
Это - инструментальная панель, которая должна быть видна только зарегистрированным пользователям. В данном руководстве это будет ваша секретная страница.
#index.handlebars Site Authentication!
Welcome aboard.
Для приложения необходим макет. Ниже приведен макет, который будете использовать.
#layout/layout.handlebars Site Authentication {{#if success_messages }} {{success_messages}} {{/if}} {{#if error_messages }} {{error_messages}} {{/if}} {{> navbar}} {{{body}}}
Вам понадобится страница входа для зарегистрированных пользователей.
#views/login.handlebars
Please sign in
Email address
Password
Sign in
Файл notFound .handlebars будет использоваться в качестве страницы для показа ошибок.
#views/notFound.handlebars Error
Ваша страница для регистрации должна выглядеть следующим образом.
Please sign up
Email address
Username
Password
Confirm Password
Sign up
Последний файл для ваших представлений с кодом навигационной панели приводится ниже.
#partials/navbar.handlebars Site Authentication
После этого вы готовы к рассмотрению некоторых сложных частей приложения.
Проверка данныхВам необходима будет модель User. Исходя из кода для представлений выше, вы можете прийти к заключению, что для модели User необходимы свойства email, username и password. Создайте папку под названием models и файл в ней под названием user.js .
#models/user.js // 1 const mongoose = require("mongoose") const Schema = mongoose.Schema const bcrypt = require("bcryptjs") // 2 const userSchema = new Schema({ email: String, username: String, password: String }, { // 3 timestamps: { createdAt: "createdAt", updatedAt: "updatedAt" } }) // 4 const User = mongoose.model("user", userSchema) module.exports = User
Не стоит сохранять пароли пользователей в незашифрованном виде. Вот что следует сделать, когда пользователь вводит пароль в незашифрованном виде при регистрации. Пароль в незашифрованном виде должен быть захеширован при помощи соли (* в криптографии - случайное число или текст, которые добавляются к данным, шифруемым с помощью пароля), которая будет сгенерирована вашим приложением (используя модуль bcryptjs). Этот захешированный пароль затем сохраняется в базу данных.
Звучит замечательно, неправда ли? Давайте реализуем это в файле user.js
#models/user.js module.exports.hashPassword = async (password) => { try { const salt = await bcrypt.genSalt(10) return await bcrypt.hash(password, salt) } catch(error) { throw new Error("Hashing failed", error) } }
Вы только что создали метод, который будет вызван при регистрации пользователя. В метод передается незашифрованный пароль, введенный пользователем. Как я упоминал ранее, пароль в незашифрованном виде должен быть захеширован при помощи соли, которая будет сгенерирована вашим приложением. Захешированный пароль будет возвращен как пароль для пользователя.
Маршруты Index и UsersСоздайте новую папку под названием routes . В этой новой папке создайте два новых файла: index.js и users.js .
Файл index.js будет очень простым. В нем будет вызываться на исполнение представление index вашего приложения. Помните, как вы настроили промежуточное ПО для ваших маршрутов в вашем файле app.js , когда вы сделали следующее:
App.use("/", require("./routes/index")) app.use("/users", require("./routes/users"))
Итак, ваш маршрут в файле index.js, просто исполняющий страницу index, должен выглядеть следующим образом:
#routes/index.js const express = require("express") const router = express.Router() router.get("/", (req, res) => { res.render("index") }) module.exports = router
Теперь переходим к маршруту users. Пока что в этом файле для маршрута будут выполняться четыре действия:
Вот как выглядит код.
#routes/users.js const express = require("express"); const router = express.Router() const Joi = require("joi") const passport = require("passport") const User = require("../models/user") //validation schema const userSchema = Joi.object().keys({ email: Joi.string().email().required(), username: Joi.string().required(), password: Joi.string().regex(/^{6,30}$/).required(), confirmationPassword: Joi.any().valid(Joi.ref("password")).required() }) router.route("/register") .get((req, res) => { res.render("register") }) .post(async (req, res, next) => { try { const result = Joi.validate(req.body, userSchema) if (result.error) { req.flash("error", "Data entered is not valid. Please try again.") res.redirect("/users/register") return } const user = await User.findOne({ "email": result.value.email }) if (user) { req.flash("error", "Email is already in use.") res.redirect("/users/register") return } const hash = await User.hashPassword(result.value.password) delete result.value.confirmationPassword result.value.password = hash const newUser = await new User(result.value) await newUser.save() req.flash("success", "Registration successfully, go ahead and login.") res.redirect("/users/login") } catch(error) { next(error) } }) module.exports = router
Давайте рассмотрим подробнее, что происходит при поступлении запросов по методу POST .
Значения, введенные в форме для регистрации, доступны через свойство req.body и выглядят подобным образом:
Value: { email: "[email protected]", username: "izu", password: "chinedu", confirmationPassword: "chinedu" },
Это свойство проверяется с помощью схемы userSchema , которую вы создали ранее. Введенные пользователем значения присваиваются константе под названием result.
В случае ошибки при проверке данных сообщение об ошибке отображается пользователю и происходит перенаправление пользователя на страницу регистрации.
В ином случае мы пытаемся установить, существует ли пользователь с тем же адресом электронной почты, поскольку нам не хотелось бы иметь двух или более пользователей с одинаковым адресом. В случае совпадения адресов электронной почты пользователь информируется, что его адрес уже используется.
В ином случае следующим этапом является хеширование пароля. В этот момент вы и вызываете метод hashPassword , который определили в вашем файле user.js. Новый захешированный пароль присваивается константе hash.
Нет необходимости в сохранении confirmationPassword в базу данных. Поэтому это свойство удаляется. Доступный в переменной пароль по прежнему незашифрован. Поскольку не следует сохранять незашифрованный пароль в вашей базе данных, важно заново присвоить в качестве значения пароля созданный ранее хеш. Это осуществляется при помощи следующей строки кода:
Result.value.password = hash
Экземпляр нового пользователя сохраняется в базу данных. Выводится флэш-сообщение, сообщающее, что регистрация прошла успешно, и пользователь перенаправляется на страницу входа в приложение.
Запустите ваш сервер, выполнив в консоли команду:
Node app.js
Перейдите в вашем браузере на страницу http://localhost:5000 , и вы должны будете увидеть ваше новое приложение.
ЗаключениеТеперь вы знаете, как реализовать возможность регистрации в Node-приложениях. Вы поняли важность проверки пользовательских данных и процесс ее осуществления при помощи Joi. Также вы использовали модуль bcryptjs для подсаливания и хеширования вашего пароля.
В этой статье я попробую рассказать о том, как с помощью node.js и connect сделать простой сайт с авторизацией. Т.е. такой, где часть контента доступна всем, а часть - только зарегистрированным пользователям. Поскольку express.js основан на connect, практически все, что здесь написано, относится и к нему тоже.
Допустим, что вы уже знаете, что такое node.js и как с ним работать. Также допустим, что у вас уже есть простенький сайт с основной страницей и парой дополнительных. Вот - исходники такого сайта, пример для этой статьи.
- уникальный идентификатор сессии
- срок действия
- имя пользователя (если он его введет)
- другая служебная информация, необходимая для идентификации: ip-адрес, user agent
- любая другая информация, которую сайт связывает с текущим пользователем. Например, электронный магазин может хранить в сессии список товаров, добавленных в корзину.
app.use(connect.cookieParser()); app.use(connect.session({ secret: "your secret here"}));
Порядок имеет значение, сами правила должны быть определены до задания маршрутов. Первое правило обеспечивает работу с куками в общем. Второе добавляет к обычному request поле session , через которое будут доступны данные сессии (дальше с примерами станет понятнее).
Connect.session получает такие параметры:
- secret - фраза, которая используется для шифрования информации в cookies.
- store - обьект, который будет использоваться для хранения данных сессии. По умолчанию connect хранит все данные в памяти, но, естественно, в реальных приложениях так делать нельзя. Есть готовые решения для mongodb , redis , MySQL и т.д.
- cookie - набор параметров cookie. Самый важный - maxAge , время жизни в миллисекундах (или null)
if ((request.body.login==="Thor")&&(request.body.password==="111")) { request.session.authorized = true; request.session.username = request.body.login; console.log("Thor is here!"); }
В принципе, хватило бы одной переменной username (так делает автор вот этой статьи). Но тогда проверка, авторизирован ли пользователь, будет выглядеть некрасиво:
if (typeof req.session.username == "undefined") { // не залогинен, перенаправить на форму ввода пароля }
Когда пользователь захочет разлогиниться, достаточно будет просто удалить добавленные поля:
delete req.session.authorized; delete req.session.username ;
Для полной очистки есть метод session.destroy() . Он удаляет session из текущего запроса, а при следующем это поле будет сгенерировано заново.Контроль доступа Наиболее очевидное решение - проверять request.session.authorized всякий раз, когда нужно сгенерировать защищенную страницу. Собственно, так и делают в статье , на которую я уже ссылался. Проблема в том, это противоречит «слоистой» идеологии connect . Лучше задать специальное правило, которое будет проверять права пользователя и, если что не так, перенаправлять его на страницу ошибки. Идея описана , в нашем случае будет
// адреса, которые поддерживает наш сайт; var siteUrls = [ {pattern:"^/login/?$", restricted: false} , {pattern:"^/logout/?$", restricted: true} , {pattern:"^/$", restricted: false} , {pattern:"^/single/\\w+/?$", restricted: true} ]; function authorizeUrls(urls) { function authorize(req, res, next) { var requestedUrl = url.parse(req.url).pathname; for (var ui in urls) { var pattern = urls.pattern; var restricted = urls.restricted; if (requestedUrl.match(pattern)) { if (restricted) { if (req.session.authorized) { // если все хорошо, просто переходим к следующим правилам next(); return; } else{ // пользователь не авторизирован, отправляем его на страницу логина res.writeHead(303, {"Location": "/login"}); res.end(); return; } } else { next(); return; } } } // сюда мы попадаем, только если в цикле не нашлось совпадений console.log("common 404 for ", req.url); res.end("404: there is no " + req.url + " here"); } return authorize ; } app.use("/", authorizeUrls(siteUrls));
Все. Надеюсь, это кому-нибудь поможет.
Теги: Добавить метки
В этой главе вы узнаете, как реализовать стратегию локальной аутентификации в Node.js приложении с использованием Passport.js и Redis.
This article was translated to Russian by Andrey Melikhov, a front-end developer from Yandex.Money and editor of the collective blog about front-end, devSchacht . Find Andrey on: Twitter , GitHub , Medium & SoundCloud
Перевод этой статьи сделан Андреем Мелиховым, фронтенд-разработчиком из компании Яндекс.Деньги, редактором коллективного блога о фронтенде, devSchacht . Twitter | GitHub | Medium | SoundCloud
Технологии, которые мы будем использовать
Прежде чем перейти к написанию кода, давайте рассмотрим новые технологии, которые мы будем использовать в этой главе.
Что такое Passport.js?Простая, ненавязчивая аутентификация для Node.js - passportjs.org
Passport - это middleware для проверки подлинности, которую мы собираемся использовать для управления сессиями.
Что такое Redis?Redis это опенсорс (лицензии BSD) хранилище структур данных в оперативной памяти, используемое как база данных, кэш и брокер сообщений - redis.io
Мы собираемся хранить информацию о сессии пользователя в Redis, а не в памяти процесса. Таким образом, наше приложение будет намного проще масштабировать.
Демонстрационное приложениеДля демонстрационных целей создадим приложение, которое умеет только следующее:
- предоставляет форму входа
- предоставляет две защищённые страницы:
- страницу профиля
- безопасные заметки
Вы уже научились структурировать Node.js-проекты в предыдущей главе Node Hero, поэтому давайте использовать эти знания!
Мы собираемся использовать следующую структуру:
├── app | ├── authentication | ├── note | ├── user | ├── index.js | └── layout.hbs ├── config | └── index.js ├── index.js └── package.json
Как вы можете видеть, мы организуем файлы и каталоги вокруг функций. У нас будет страница пользователя, страница заметок и некоторые функции, связанные с проверкой подлинности.
Процесс аутентификации в Node.jsНаша цель - реализовать в нашем приложении следующий процесс аутентификации:
Чтобы настроить такую стратегию аутентификации, выполните следующие три действия:
Мы будем использовать Express для серверной среды - вы можете узнать больше на эту тему, перечитав главу «Ваш первый сервер на Node.js».
// file:app/index.js const express = require("express") const passport = require("passport") const session = require("express-session") const RedisStore = require("connect-redis")(session) const app = express() app.use(session({ store: new RedisStore({ url: config.redisStore.url }), secret: config.redisStore.secret, resave: false, saveUninitialized: false })) app.use(passport.initialize()) app.use(passport.session())
Что мы тут делаем?
Прежде всего, нам нужны все зависимости, которые требуются для управления сессией. После этого мы создали новый экземпляр из модуля express-session , который будет хранить наши сессии.
Для хранения сессий мы используем Redis, но вы можете использовать любые другие, такие как MySQL или MongoDB.
Шаг 2: Настройка PassportPassport - отличный пример библиотеки, использующей плагины. В этом уроке мы добавляем модуль passport-local , который добавляет простую локальную стратегию аутентификации с использованием имён пользователей и паролей.
Для простоты в этом примере (см. ниже) мы не используем базу данных, вместо неё используется экземпляр объекта пользователя в памяти. В реальных приложениях findUser будет искать пользователя в базе данных.
File:app/authenticate/init.js const passport = require("passport") const LocalStrategy = require("passport-local").Strategy const user = { username: "test-user", password: "test-password", id: 1 } passport.use(new LocalStrategy(function(username, password, done) { findUser(username, function (err, user) { if (err) { return done(err) } if (!user) { return done(null, false) } if (password !== user.password) { return done(null, false) } return done(null, user) }) }))
Как только findUser возвращается с нашим объектом пользователя, остаётся только сравнить введённый пользователем и реальный пароль, чтобы увидеть, есть ли совпадение.
Если они совпадают, мы разрешаем пользователю войти (возвращая объект пользователь в passport - return done(null, user)), если нет - возвращаем ошибку авторизации(путём возврата null - return done(null)).
Примечание переводчика: В настоящих приложениях стараются никогда не хранить пароли пользователей в открытом виде. В базу данных записывают хэш пароля и сравнивают его с хэшом значения, введённого пользователем.
Шаг 3: Добавляем защищённые разделы на сайтеЧтобы добавить защищенные разделы на сайт, мы используем шаблон middleware Express. Для этого сначала создадим middleware для аутентификации:
// file:app/user/init.js const passport = require("passport") app.get("/profile", passport.authenticationMiddleware(), renderProfile)
Она имеет только одну задачу - если пользователь аутентифицирован (имеет правильные cookie-файлы), она просто вызывает следующую middleware. В противном случае пользователь перенаправляется на страницу, где он может войти в систему.
Использование этой middleware также просто, как добавление любой другой middleware в определение роута.
// file:app/authentication/middleware.js function authenticationMiddleware () { return function (req, res, next) { if (req.isAuthenticated()) { return next() } res.redirect("/") } }
РезюмеВ этом разделе учебника по Node.js вы узнали, как добавить базовую аутентификацию в ваше приложение. Позже вы можете расширить его с помощью различных стратегий аутентификации, таких как Facebook или Twitter. Вы можете найти больше стратегий по адресу http://passportjs.org/ .
Полный рабочий пример нашего демонстрационного приложения вы можете найти на GitHub: https://github.com/RisingStack/nodehero-authentication
Следующая глава Node Hero будет посвящена юнит-тестированию Node.js приложений. Вы узнаете такие концепции, как юнит-тестирование, пирамида тестирования, дублёры и многое другое!
Gergely NemethCo-founder of RisingStack
Please enable JavaScript to view the Node Hero: Глава 8В этой главе вы узнаете, как реализовать стратегию локальной аутентификации в Node.js приложении с использованием Passport.js и Redis.
Технологии, которые мы будем использоватьПрежде чем перейти к написанию кода, давайте рассмотрим новые технологии, которые мы будем использовать в этой главе.
Что такое Passport.js?Простая, ненавязчивая аутентификация для Node.js - passportjs.org
Passport - это middleware для проверки подлинности, которую мы собираемся использовать для управления сессиями.
Что такое Redis?Redis это опенсорс (лицензии BSD) хранилище структур данных в оперативной памяти, используемое как база данных, кэш и брокер сообщений - redis.io
Мы собираемся хранить информацию о сессии пользователя в Redis, а не в памяти процесса. Таким образом, наше приложение будет намного проще масштабировать.
Демонстрационное приложениеДля демонстрационных целей создадим приложение, которое умеет только следующее:
- предоставляет форму входа
- предоставляет две защищённые страницы:
- страницу профиля
- безопасные заметки
Вы уже научились структурировать Node.js-проекты в предыдущей главе Node Hero, поэтому давайте использовать эти знания!
Мы собираемся использовать следующую структуру:
├── app
| ├── authentication
| ├── note
| ├── user
| ├── index.js
| └── layout.hbs
├── config
| └── index.js
├── index.js
└── package.json
Как вы можете видеть, мы организуем файлы и каталоги вокруг функций. У нас будет страница пользователя, страница заметок и некоторые функции, связанные с проверкой подлинности.
(Вы можете скачать исходный код по ссылке https://github.com/RisingStack/nodehero-authentication )
Процесс аутентификации в Node.jsНаша цель - реализовать в нашем приложении следующий процесс аутентификации:
Чтобы настроить такую стратегию аутентификации, выполните следующие три действия:
Мы будем использовать Express для серверной среды - вы можете узнать больше на эту тему, перечитав главу «Ваш первый сервер на Node.js».
// file:app/index.js
const express = require("express")
const session = require("express-session")
const RedisStore = require("connect-redis")(session)
const app = express()
app.use(session({
store: new RedisStore({
url: config.redisStore.url
}),
secret: config.redisStore.secret,
resave: false,
saveUninitialized: false
}))
app.use(passport.initialize())
app.use(passport.session())
Что мы тут делаем?
Прежде всего, нам нужны все зависимости, которые требуются для управления сессией. После этого мы создали новый экземпляр из модуля express-session , который будет хранить наши сессии.
Для хранения сессий мы используем Redis, но вы можете использовать любые другие, такие как MySQL или MongoDB.
Шаг 2: Настройка PassportPassport - отличный пример библиотеки, использующей плагины. В этом уроке мы добавляем модуль passport-local , который добавляет простую локальную стратегию аутентификации с использованием имён пользователей и паролей.
Для простоты в этом примере (см. ниже) мы не используем базу данных, вместо неё используется экземпляр объекта пользователя в памяти. В реальных приложениях findUser будет искать пользователя в базе данных.
// file:app/authenticate/init.js
const passport = require("passport")
const LocalStrategy = require("passport-local").Strategy
const user = {
username: "test-user",
password: "my-password",
id: 1
}
passport.use(new LocalStrategy(
function(username, password, done) {
findUser(username, function (err, user) {
if (err) {
return done(err)
}
if (!user) {
return done(null, false)
}
if (password !== user.password) {
return done(null, false)
}
return done(null, user)
})
}
))
Как только findUser возвращается с нашим объектом пользователя, остаётся только сравнить введённый пользователем и реальный пароль, чтобы увидеть, есть ли совпадение.
Если они совпадают, мы разрешаем пользователю войти (возвращая объект пользователь в passport - return done(null, user)), если нет - возвращаем ошибку авторизации(путём возврата null - return done(null)).
Примечание переводчика: В настоящих приложениях стараются никогда не хранить пароли пользователей в открытом виде. В базу данных записывают хэш пароля и сравнивают его с хэшом значения, введённого пользователем.
Шаг 3: Добавляем защищённые разделы на сайтеЧтобы добавить защищенные разделы на сайт, мы используем шаблон middleware Express. Для этого сначала создадим middleware для аутентификации:
// file:app/user/init.js
const passport = require("passport")
app.get("/profile", passport.authenticationMiddleware(), renderProfile)
Она имеет только одну задачу - если пользователь аутентифицирован (имеет правильные cookie-файлы), она просто вызывает следующую middleware. В противном случае пользователь перенаправляется на страницу, где он может войти в систему.
Использование этой middleware также просто, как добавление любой другой middleware в определение роута.
// file:app/authentication/middleware.js
function authenticationMiddleware () {
return function (req, res, next) {
if (req.isAuthenticated()) {
return next()
}
res.redirect("/")
}
}
В этом разделе учебника по Node.js вы узнали, как добавить базовую аутентификацию в ваше приложение. Позже вы можете расширить его с помощью различных стратегий аутентификации, таких как Facebook или Twitter. Вы можете найти больше стратегий по адресу http://passportjs.org/ .
Полный рабочий пример нашего демонстрационного приложения вы можете найти на GitHub: https://github.com/RisingStack/nodehero-authentication
Следующая глава Node Hero будет посвящена юнит-тестированию Node.js приложений. Вы узнаете такие концепции, как юнит-тестирование, пирамида тестирования, дублёры и многое другое!
основан на connect, практически все, что здесь написано, относится и к нему тоже.Допустим, что вы уже знаете, что такое node.js и как с ним работать. Также допустим, что у вас уже есть простенький сайт с основной страницей и парой дополнительных. Вот - исходники такого сайта, пример для этой статьи.
Теория
Есть такое понятие, как сессия - период времени, пока пользователь находится на сайте. Сессия начинается, когда пользователь впервые открывает сайт в браузере и заканчивается, когда у нее истечет срок действия (или когда сайт захочет ее прервать). С каждой сессией связывается определенный набор данных:
- уникальный идентификатор сессии
- срок действия
- имя пользователя (если он его введет)
- другая служебная информация, необходимая для идентификации: ip-адрес, user agent
- любая другая информация, которую сайт связывает с текущим пользователем. Например, электронный магазин может хранить в сессии список товаров, добавленных в корзину.
Для решения нашей задачи нужны две таблицы в базе данных: одна для хранения данных сессии, другая - для информации о пользователях. На самом деле, в данном случае говорить «таблица БД» не совсем правильно, информация может находиться в разных местах. Например, всю параметры сессии можно хранить в cookies (или в памяти приложения, хотя это и нехорошо). Данные о пользователе могут поступать извне, если он заходит с помощью OpenID/OAuth.
Connect
Всю работу по созданию сессии connect берет на себя. Для этого нужно добавить два правила:
app.use(connect.cookieParser()); app.use(connect.session({ secret: "your secret here" }));
Порядок имеет значение, сами правила должны быть определены до задания маршрутов. Первое правило обеспечивает работу с куками в общем. Второе добавляет к обычному request поле session , через которое будут доступны данные сессии (дальше с примерами станет понятнее).
connect.session получает такие параметры:
- secret - фраза, которая используется для шифрования информации в cookies.
- store - обьект, который будет использоваться для хранения данных сессии. По умолчанию connect хранит все данные в памяти, но, естественно, в реальных приложениях так делать нельзя. Есть готовые решения для mongodb , redis , MySQL и т.д.
- cookie - набор параметров cookie. Самый важный - maxAge , время жизни в миллисекундах (или null)
Авторизация
Как уже было сказано, connect будет добавлять поле session к каждому запросу, но по умолчанию там ничего интересного нет. Если мы каким-то образом «узнаем» пользователя (собственно, если он введет правильный пароль), мы должны будем сами добавить информацию о нем к сессии. Приблизительно так:
if ((request.body.login==="Thor" )&&(request.body.password==="111" )) { request.session.authorized = true ; request.session.username = request.body.login; console.log("Thor is here!" ); }
В принципе, хватило бы одной переменной username (так делает автор вот этой статьи ). Но тогда проверка, авторизирован ли пользователь, будет выглядеть некрасиво:
if (typeof req.session.username == "undefined" ) { // не залогинен, перенаправить на форму ввода пароля }
Когда пользователь захочет разлогиниться, достаточно будет просто удалить добавленные поля:
delete req.session.authorized; delete req.session.username ;
Для полной очистки есть метод session.destroy() . Он удаляет session из текущего запроса, а при следующем это поле будет сгенерировано заново.
Контроль доступа
Наиболее очевидное решение - проверять request.session.authorized всякий раз, когда нужно сгенерировать защищенную страницу. Собственно, так и делают в статье , на которую я уже ссылался. Проблема в том, это противоречит «слоистой» идеологии connect . Лучше задать специальное правило, которое будет проверять права пользователя и, если что не так, перенаправлять его на страницу ошибки. Идея описана , в нашем случае будет
// адреса, которые поддерживает наш сайт; var siteUrls = [ {pattern:"^/login/?$" , restricted: false } , {pattern:"^/logout/?$" , restricted: true } , {pattern:"^/$" , restricted: false } , {pattern:"^/single/\\w+/?$" , restricted: true } ]; function authorizeUrls (urls) { function authorize (req, res, next) { var requestedUrl = url.parse(req.url).pathname; for (var ui in urls) { var pattern = urls.pattern; var restricted = urls.restricted; if (requestedUrl.match(pattern)) { if (restricted) { if (req.session.authorized) { // если все хорошо, просто переходим к следующим правилам next(); return ; } else { // пользователь не авторизирован, отправляем его на страницу логина res.writeHead(303 , {"Location" : "/login" }); res.end(); return ; } } else { next(); return ; } } } // сюда мы попадаем, только если в цикле не нашлось совпадений console.log("common 404 for " , req.url); res.end("404: there is no " + req.url + " here" ); } return authorize ; } app.use("/" , authorizeUrls(siteUrls));
Все. Надеюсь, это кому-нибудь поможет.