Как аутентифицировать пользователей в вашем приложении Node с помощью файлов cookie и Passport.js
Изучение того, как аутентифицировать пользователей в приложении - одна из первых вещей, которую вы изучаете в любом курсе, посвященном серверным технологиям.
И это один из первых шагов, которые вы делаете при создании приложения для социальных сетей, приложения для обучения на онлайн-курсах и так далее.
В этой статье мы рассмотрим базовые концепции аутентификации для начинающих.
Как аутентифицировать пользователя с помощью файлов cookie
Прежде всего, давайте создадим простое приложение, которое аутентифицирует пользователей с использованием классического метода имени пользователя и пароля - сейчас мы не будем беспокоиться о подключениях к базе данных.
Каждый раз, когда мы пытаемся загрузить страницу, браузер отправляет запрос на сервер, который отвечает соответствующим образом.
Первоначально, когда пользователь заходит на сайт, ему предлагается ввести зарегистрированное имя пользователя и пароль. После передачи они сообщаются серверу, поэтому последующие запросы к серверу не требуют повторного подтверждения их личности.
Но как сервер будет отслеживать, какие пользователи уже аутентифицированы, а какие нет? Вот тут и пригодятся куки.
Согласно w3.org, файлы cookie определяются как:
«Биты текста, хранящиеся на клиентском компьютере и отправленные с HTTP-запросом на веб-сайт, для которого они были созданы».
Файлы cookie создаются и сохраняются после того, как пользователь входит в систему, и с ними консультируются перед выполнением последовательных запросов. Затем они истекают в соответствии с указанным сроком.
let express=require('express')
let cookie_parser=require('cookie-parser')
let app=express()
app.use(cookie_parser('1234'))
Сначала мы настраиваем наше приложение Express и включаем cookie-parser
. Он анализирует заголовок cookie запроса и добавляет его в req.cookies
или req.signedCookies
(если используются секретные ключи) для дальнейшей обработки.
cookie-parser
принимает в качестве аргумента секретный ключ, который будет использоваться для создания HMAC текущего значения cookie. Если значение будет изменено позже, оно будет обнаружено, поскольку подпись, сделанная во время создания, не соответствует текущей подписи.
Затем, когда пользователь посещает соответствующий URL-адрес (например, / login или что-то подобное), нам нужно выполнить некоторые проверки. Предположим, пользователь входит в систему впервые.
let cookie_Stuff=req.signedCookies.user
//But the user is logging in for the first time so there won't be any appropriate signed cookie for usage.
if(!cookie_Stuff)//True for our case
{
let auth_Stuff=req.headers.authorization
if(!auth_Stuff)//No authentication info given
{
res.setHeader("WWW-Authenticate", "Basic")
res.sendStatus(401)
}
Мы используем заголовок ответа WWW-Authenticate, чтобы определить метод аутентификации, который следует использовать для получения доступа к ресурсу («Basic» метод).
Ответ от клиента состоит из имени пользователя и пароля, разделенных двоеточием. Он закодирован в base64 и прикреплен к заголовку авторизации запроса.
У пользователя запрашивают информацию для аутентификации, которая извлекается и проверяется. На самом деле нам нужно проверять по базе данных, но для простоты мы сейчас делаем наивную проверку.
Если указаны правильные значения, мы устанавливаем соответствующий файл cookie. Если нет, то мы снова запрашиваем у пользователя:
else
{
step1=new Buffer.from(auth_Stuff.split(" ")[1], 'base64')
//Extracting username:password from the encoding Authorization: Basic username:password
step2=step1.toString().split(":")
//Extracting the username and password in an array
if(step2[0]=='admin' && step2[1]=='admin')
{
//Correct username and password given
console.log("WELCOME ADMIN")
//Store a cookie with name=user and value=username
res.cookie('user', 'admin', {signed: true})
res.send("Signed in the first time")
}
else
{
//Wrong authentication info, retry
res.setHeader("WWW-Authenticate", "Basic")
res.sendStatus(401)
}
}
}
А что насчет следующего раза, когда наш пользователь сделает запрос? С этого момента, пока cookie не будет очищен или не истечет срок его действия, мы проверяем значение cookie для аутентификации.
else
{//Signed cookie already stored
if(req.signedCookies.user=='admin')
{
res.send("HELLO GENUINE USER")
}
else
{
//Wrong info, user asked to authenticate again
res.setHeader("WWW-Authenticate", "Basic")
res.sendStatus(401)
}
}
})
Теперь вы знаете, как аутентифицировать пользователя с помощью файлов cookie!
Вы можете проверить файл cookie, который хранится, перейдя в раздел «Хранилище» инструментов разработчика своего браузера и перейдя на вкладку «Файлы cookie». Значение cookie и проанализированные значения отображаются отдельно в двух разделах (например, в Firefox).
Как аутентифицировать пользователя с помощью сеансов
Давайте рассмотрим аналогию, чтобы помочь нам понять сеансы по сравнению с файлами cookie. Представьте, что вы рассеянный человек, который все время забывает имена своих друзей.
Одно из решений - дать каждому другу карточку с его именем и изображением. Каждый раз, когда вы встречаетесь с ними, просто просите показать карточку, которую вы им дали, чтобы освежить свою память.
Проблема в том, что ваши друзья могут потерять эту карту. Или двое из них могут поменяться картами и подшутить над вами. Или, может быть, у вашего друга недостаточно места для хранения другой карты.
В любом случае механизм аутентификации обнаруживает признаки слабости. Но это, по сути, то, что делают файлы cookie - они хранятся на стороне клиента, и каждый раз, когда клиент делает запрос на свой сайт, cookie используется для аутентификации.
Предположим, вы делаете карточки для каждого друга и храните их при себе. Когда вы их видите, вы назначаете способ сопоставления карты с человеком для облегчения идентификации. Таким образом, информация не хранится у клиента и, следовательно, более безопасна.
Этот сценарий показывает нам, как работают сеансы. Информация о новом аутентифицированном пользователе находится на сервере, и только минимальная информация передается обратно клиенту. Таким образом, клиент может быть сопоставлен с сохраненной частью информации.
express-session
создает промежуточное ПО для сеансов, которое позволяет легко настраивать сеансы и управлять ими.
Серверное хранилище по умолчанию - это MemoryStore. Чтобы хранить информацию о сеансе в виде файлов JSON, вам потребуется хранилище файлов сеанса. Приведенный ниже код делает следующее:
- Устанавливает приложение Express
- Он сообщает промежуточному программному обеспечению запрашивать аутентификацию, если ничего не указано, и в противном случае проверяет, совпадают ли имя пользователя и пароль.
- Если нет, ему снова нужно сделать тот же запрос на аутентификацию, иначе сеанс будет установлен.
- Затем он добавляет имя пользователя в качестве атрибута пользователя и впоследствии проверяет его.
Опять же, это всего лишь простой пример - информация, которую необходимо проверить на соответствие заданным данным, должна как минимум храниться в базе данных.
let app=express()
app.use(session({
store: new File_Store,
secret: 'hello world',
resave: true,
saveUninitialized: true
}))
app.use('/', (req,res,next)=>{
if(!req.session.user)
{
console.log("Session not set-up yet")
if(!req.headers.authorization)
{
console.log("No auth headers")
res.setHeader("WWW-Authenticate", "Basic")
res.sendStatus(401)
}
else
{
auth_stuff=new Buffer.from(req.headers.authorization.split(" ")[1], 'base64')
step1=auth_stuff.toString().split(":")
console.log("Step1: ", step1)
if(step1[0]=='admin' && step1[1]=='admin')
{
console.log('GENUINE USER')
req.session.user='admin'
res.send("GENUINE USER")
}
else
{
res.setHeader("WWW-Authenticate", "Basic")
res.sendStatus(401)
}
}
}
Как аутентифицировать пользователей с помощью промежуточного программного обеспечения Passport.js
До сих пор мы видели, как аутентифицировать пользователей с помощью файлов cookie и сеансов. Теперь мы увидим третий метод аутентификации.
Passport.js - это промежуточное ПО для аутентификации в Node, которое позволяет аутентифицировать пользователей с помощью сеансов и OAuth. Он также позволяет создавать собственные стратегии и многое другое.
let passport=require('passport')
let bcrypt=require('bcrypt-nodejs')
let User_Obj=require('./Set_Up_Database_Stuffs')
const local_strategy=require('passport-local').Strategy
Этот код устанавливает все необходимые модули для определения подходящей локальной стратегии. Стратегия passport-local
позволяет аутентификацию с помощью имени пользователя и пароля.
Убедитесь, что имя элемента ввода формы для имени пользователя - «username», а для пароля - «password». Хотя это звучит очень интуитивно, это доставило мне много проблем, так как я упустил эту часть из виду. Теперь это одна из тех вещей, которые я вряд ли снова упущу из виду (по крайней мере, в ближайшем будущем).
Вы также можете изменить имена полей по умолчанию, передав JSON перед функцией обратного вызова в вызове local_strategy
, где структура JSON usernameField
: «Некоторое новое имя для этого поля» и passwordField
: «Некоторое новое имя для этого поля».
passport.use(new local_strategy(
async (username, password, done)=>{
console.log("Here inside local_strategy" ,username, password)
try
{
let row1=await User_Obj.findOne({username: username})
console.log(row1)
//row1 should be the tuple from database where the username field matches the username supplied.
if(row1==null)
{
console.log("NO RECORDS FOUND")
return done(null, false)
}
else
{
console.log("Record found")
console.log(row1)
if(bcrypt.compareSync(password, row1.password))//Compare plaintext password with the hash
{
console.log("The passwords match")
console.log("Finished authenticate local")
return done(null, row1)
}
else
{
console.log("The passwords don't match")
return done(null, false)
}
}
}
catch(err){
console.log("Some error here")
return done(err)}
}
));
Приведенные выше строки являются упрощенной реализацией local-strategy
, где данные проверяются из базы данных с именем пользователя в качестве поля.
app.post('/auth', passport.authenticate('local', {successRedirect: 'articles', failureRedirect: '/failurepage'}))
//Triggers the local strategy. If successful, redirect to articles page else show failure page
app.post('/donesignup', objForUrlencoded, async (req,res)=>{
console.log(req.body)
try
{
let row1=await User_Obj.findOne({username: req.body.username})
console.log(row1)
if(row1!=null)
{
console.log("That username already exists")
res.render('signup')
}
else
{
console.log(bcrypt.hashSync(req.body.password[0], bcrypt.genSaltSync(8), null))//Get the hash of the password to store it in the database
let save_this=User_Obj({username: req.body.username, password: bcrypt.hashSync(req.body.password[0], bcrypt.genSaltSync(8), null)})
console.log(save_this)
save_this.save()
console.log("SAVED IT")//Save it to database
}
}
catch(err){}
})
Всякий раз, когда пользователь обращается к маршруту /auth, он запускает локальную стратегию, которая выполняется, как указано. Если во время аутентификации произошел сбой, он перенаправляется на страницу сбоя. В противном случае он перенаправляет на страницу статей (или на любую другую страницу, которая вам нужна).
В почтовом запросе на /donesignup он проверяет, существует ли уже имя пользователя. Если нет, то он добавляет его как кортеж в базу данных, где поля - это имя пользователя и хэш заданного пароля.
Заключение
На этом я завершаю обзор различных методов аутентификации в Node.
Используемый здесь код далек от идеала, но я надеюсь, что он поможет кому-то, кто только начал изучать аутентификацию и чувствовал себя подавленным.