Статьи

Использование веб-токенов JSON с Node.js

Фреймворки и библиотеки, такие как Ember, Angular и Backbone, являются частью тенденции к созданию более совершенных и более сложных клиентов веб-приложений. Вследствие этого серверные компоненты освобождаются от многих своих традиционных обязанностей, по сути становясь все более похожими на API. Этот подход API позволяет в большей степени отделить традиционные части «переднего конца» и «внутреннего конца» приложения. Один набор разработчиков может создать серверную часть независимо от внешних инженеров, с дополнительным преимуществом упрощает тестирование. Этот подход также значительно упрощает создание, скажем, мобильного приложения, которое использует тот же сервер, что и ваше веб-приложение.

Одной из проблем при предоставлении API является аутентификация. В традиционных веб-приложениях сервер отвечает на успешный запрос аутентификации, выполняя две вещи. Во-первых, он создает сеанс с использованием некоторого механизма хранения. Каждый сеанс имеет свой собственный идентификатор — обычно длинную, полуслучайную строку — который используется для получения информации о сеансе по будущим запросам. Во-вторых, эта информация отправляется клиенту посредством заголовков, указывающих ему установить cookie. Браузер автоматически присоединяет файл cookie идентификатора сеанса ко всем последующим запросам, позволяя серверу идентифицировать пользователя, извлекая соответствующий сеанс из хранилища. Вот как традиционные веб-приложения обходят тот факт, что HTTP не имеет состояния.

API-интерфейсы должны быть разработаны так, чтобы они действительно не сохраняли состояния. Это означает отсутствие методов входа в систему или выхода из системы, а также никаких сеансов. Разработчики API также не могут полагаться на файлы cookie, поскольку нет гарантии, что запросы будут отправляться через веб-браузер. Понятно, что нам нужен альтернативный механизм. В этой статье рассматривается один из возможных механизмов, предназначенных для решения этой проблемы, — JSON Web Tokens или JWT (произносится как jots). В примерах, приведенных в этой статье, используется среда Node’s Express на стороне сервера и Backbone на клиенте.

Фон

Давайте кратко рассмотрим несколько распространенных подходов к обеспечению безопасности API.

Одним из них является использование HTTP Basic Authentication. Определенный в официальной спецификации HTTP, это, по сути, включает в себя установку заголовка ответа сервера, который указывает на необходимость аутентификации. Клиент должен ответить, приложив свои учетные данные, включая пароль, к каждому последующему запросу. Если учетные данные совпадают, пользовательская информация становится доступной для серверного приложения как переменная.

Второй подход очень похож, но использует собственный механизм аутентификации приложения. Обычно это включает проверку предоставленных учетных данных по сравнению с данными в хранилище. Как и в случае HTTP Basic Authentication, для этого необходимо, чтобы учетные данные пользователя предоставлялись при каждом вызове.

Третий подход — OAuth (или OAuth2). Разработанный в значительной степени для аутентификации на сторонних сервисах, его реализация может быть довольно сложной, по крайней мере, на стороне сервера.

Четвертый подход — использование токенов. Об этом мы и рассмотрим в этой статье. Мы рассмотрим реализацию, которая использует JavaScript как на передней, так и на задней части.

Токен-подход

Вместо предоставления учетных данных, таких как имя пользователя и пароль, при каждом запросе, мы можем разрешить клиенту обмениваться действительными учетными данными для токена. Этот токен предоставляет клиенту доступ к ресурсам на сервере. Токены обычно намного длиннее и запутаннее пароля. Например, JWT, с которыми мы будем иметь дело, имеют порядок ~ 150 символов. Как только токен получен, он должен отправляться при каждом вызове API. Однако это все же более безопасно, чем отправка имени пользователя и пароля при каждом запросе, даже через HTTPS.

Думайте о жетоне как пропуск безопасности. По прибытии вы идентифицируете себя на стойке регистрации здания с ограниченным доступом (укажите свое имя пользователя и пароль), и, если вас удастся идентифицировать, вы получите пропуск безопасности. Когда вы перемещаетесь по зданию (пытаетесь получить доступ к ресурсам, делая вызовы API), вы должны показать свой пропуск, а не проходить процедуру первоначальной идентификации заново.

О JWTs

JWT — это черновая спецификация , хотя по сути они на самом деле являются просто более конкретной реализацией механизма аутентификации и авторизации, который уже является обычным явлением; это обмен токенов. JWT разделен на три части, разделенные точками. JWT являются URL-безопасными, то есть их можно использовать в параметрах строки запроса.

Первая часть JWT — это закодированное строковое представление простого объекта JavaScript, которое описывает токен вместе с используемым алгоритмом хеширования. Пример ниже иллюстрирует JWT с использованием HMAC SHA-256.

{ "typ" : "JWT", "alg" : "HS256" } 

После кодирования объект становится этой строкой:

 eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9 

Вторая часть JWT образует ядро ​​токена. Он также представляет собой объект JavaScript, который содержит несколько частей информации. Некоторые из этих полей являются обязательными, а некоторые необязательными. Пример, взятый из проекта спецификации, показан ниже.

 { "iss": "joe", "exp": 1300819380, "http://example.com/is_root": true } 

Это называется набором требований JWT. Для целей этой статьи мы собираемся игнорировать третий параметр, но вы можете прочитать больше в спецификации . Свойство iss является коротким для issuer и указывает физическое или юридическое лицо, делающее запрос. Обычно это пользователь, который обращается к API. Поле exp , сокращенное до expires , используется для ограничения времени жизни токена. После кодирования токен JSON выглядит следующим образом:

 eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ 

Третья и последняя часть JWT — это подпись, сгенерированная на основе заголовка (часть первая) и тела (часть вторая). Подпись для нашего примера JWT показана ниже.

 dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk 

Получившийся полный JWT выглядит так:

 eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk 

В спецификации поддерживается ряд дополнительных, дополнительных свойств. Среди них — iat представляющая время, в которое токен был выпущен, nbf (Not Before), чтобы указать, что токен не должен быть принят раньше определенного времени, и aud (аудитория), чтобы указать получателей, для которых предназначен токен.

Обработка токенов

Мы собираемся использовать модуль JWT Simple для обработки токенов, что избавляет нас от необходимости вникать в тонкости кодирования и декодирования. Если вы действительно заинтересованы, вы можете найти больше информации в спецификации или прочитать исходный код репо.

Начните с установки библиотеки с помощью следующей команды. Помните, что вы можете автоматически добавить его в файл package.json вашего проекта, включив в --save флаг --save .

 npm install jwt-simple 

В разделе инициализации вашего приложения добавьте следующий код. Этот код импортирует Express и JWT Simple и создает новое приложение Express. В последней строке примера для переменной приложения с именем jwtTokenSecret устанавливается значение YOUR_SECRET_STRING (обязательно измените это значение на другое).

 var express = require('express'); var jwt = require('jwt-simple'); var app = express(); app.set('jwtTokenSecret', 'YOUR_SECRET_STRING'); 

Получение токена

Первое, что нам нужно сделать, это дать возможность клиенту обменять свое имя пользователя и пароль на токен. Существует два возможных подхода к этому в RESTful API. Первый — это POST запроса POST конечной точке аутентификации, когда сервер отвечает на успешный запрос токеном. В качестве альтернативы вы могли бы реализовать конечную точку, из которой клиент может получить токен, для чего необходимо предоставить свои учетные данные либо в качестве параметров запроса, либо, что еще лучше, через заголовки.

Цель этой статьи — объяснить токены аутентификации, а не основной механизм аутентификации по имени пользователя / паролю, поэтому давайте предположим, что у нас уже есть следующее, и мы уже получили username и password из запроса:

 User.findOne({ username: username }, function(err, user) { if (err) { // user not found return res.send(401); } if (!user) { // incorrect username return res.send(401); } if (!user.validPassword(password)) { // incorrect password return res.send(401); } // User has authenticated OK res.send(200); }); 

Далее нам нужно ответить на успешную попытку аутентификации с помощью токена JWT:

 var expires = moment().add('days', 7).valueOf(); var token = jwt.encode({ iss: user.id, exp: expires }, app.get('jwtTokenSecret')); res.json({ token : token, expires: expires, user: user.toJSON() }); 

Вы заметите, что jwt.encode() принимает два параметра. Первый — это объект, который сформирует тело токена. Вторая — секретная строка, которую мы определили ранее. Токен создается с использованием ранее описанных полей iss и exp . Обратите внимание, что Moment.js используется для установки срока действия до 7 дней. Метод res.json() используется для возврата JSON-представления токена клиенту.

Проверка токена

Для проверки JWT нам нужно написать некоторое промежуточное программное обеспечение, которое будет:

  1. Проверьте наличие прикрепленного токена.
  2. Попытка расшифровать его.
  3. Проверьте действительность токена.
  4. Если токен действителен, извлеките соответствующую запись пользователя и прикрепите ее к объекту запроса.

Давайте начнем с создания голого промежуточного программного обеспечения:

 // @file jwtauth.js var UserModel = require('../models/user'); var jwt = require('jwt-simple'); module.exports = function(req, res, next) { // code goes here }; 

Для максимальной гибкости мы позволим клиенту присоединять токен одним из трех способов — в качестве параметра строки запроса, параметра тела формы или в заголовке HTTP. Для последнего мы будем использовать заголовок x-access-token .

Вот код, который идет в нашем промежуточном программном обеспечении, который пытается получить токен:

 var token = (req.body && req.body.access_token) || (req.query && req.query.access_token) || req.headers['x-access-token']; 

Обратите внимание, что для доступа к req.body нам нужно сначала присоединить промежуточное программное обеспечение express.bodyParser() .

Далее, давайте попробуем декодировать JWT:

 if (token) { try { var decoded = jwt.decode(token, app.get('jwtTokenSecret')); // handle token here } catch (err) { return next(); } } else { next(); } 

Если процесс декодирования завершится неудачно, пакет JWT Simple выдаст исключение. Если это происходит или токен не был предоставлен, мы просто вызываем next() для продолжения обработки запроса — это просто означает, что мы не определили пользователя. Если действительный токен существует и декодируется, мы должны получить объект с двумя свойствами — iss содержащий идентификатор пользователя, и exp с меткой времени истечения. Давайте сначала проверим последний и отклоним токен, если срок его действия истек:

 if (decoded.exp <= Date.now()) { res.end('Access token has expired', 400); } 

Если токен все еще действителен, мы можем получить пользователя и прикрепить его к объекту запроса, как показано ниже.

 User.findOne({ _id: decoded.iss }, function(err, user) { req.user = user; }); 

Наконец, прикрепите промежуточное ПО к маршруту:

 var jwtauth = require('./jwtauth.js'); app.get('/something', [express.bodyParser(), jwtauth], function(req, res){ // do something }); 

Или, возможно, прикрепить его к куче маршрутов:

 app.all('/api/*', [express.bodyParser(), jwtauth]); 

Наше промежуточное программное обеспечение теперь проверяет запросы на поиск допустимого токена и, если он существует, присоединяет к запросу объект пользователя. Теперь должно быть довольно просто создать какое-то простое промежуточное программное обеспечение, чтобы отклонить запрос без действительного токена, хотя вы можете захотеть встроить его в тот же самый промежуточный компонент.

Это серверный элемент подхода с использованием токенов. В следующем разделе мы рассмотрим, как токены работают на стороне клиента.

Клиент

Мы предоставили простую конечную точку GET для получения токена доступа. Это достаточно просто, что нам, вероятно, не нужно вдаваться в детали — просто позвоните, передав имя пользователя и пароль (возможно, из формы), и, если запрос выполнен успешно, сохраните полученный токен где-нибудь для дальнейшего использования.

Более подробно мы рассмотрим присоединение токена к последующим вызовам. Один из способов сделать это — использовать метод ajaxSetup() . Это может быть использовано для простых вызовов Ajax или для интерфейсных сред, которые используют Ajax для связи с сервером. Например, предположим, что мы поместили наши токены доступа в локальное хранилище, используя window.localStorage.setItem('token', 'the-long-access-token') ; мы можем прикрепить токены ко всем звонкам через заголовки:

 var token = window.localStorage.getItem('token'); if (token) { $.ajaxSetup({ headers: { 'x-access-token': token } }); } 

Проще говоря, это «захватит» все запросы Ajax и, если токен находится в локальном хранилище, он присоединит его к запросу с помощью заголовка x-access-token .

Это не относится к истечению срока действия токена, но это должно быть относительно просто. Вы помните, что мы вернули метку времени истечения срока действия с токеном. Кроме того, вы можете пожелать, чтобы сервер уведомил клиента о токене с истекшим сроком действия, используя заголовки, которые указывают, что они должны повторно пройти аутентификацию.

Использование с Backbone

Давайте применим подход из предыдущего раздела к приложению Backbone. Самый простой способ сделать это — глобально переопределить Backbone.sync() как показано ниже.

 // Store "old" sync function var backboneSync = Backbone.sync // Now override Backbone.sync = function (method, model, options) { /* * "options" represents the options passed to the underlying $.ajax call */ var token = window.localStorage.getItem('token'); if (token) { options.headers = { 'x-access-token': token } } // call the original function backboneSync(method, model, options); }; 

Дополнительная безопасность

Вы можете добавить дополнительный уровень безопасности, сохраняя запись выданных токенов на сервере, а затем проверяя их по этой записи при каждом последующем запросе. Это предотвратит «подделку» токена третьей стороной, а также позволит серверу аннулировать токен. Я не буду освещать это здесь, но это должно быть относительно простым для реализации.

Резюме

В этой статье мы рассмотрели некоторые подходы к аутентификации на API, в частности, JSON Web Tokens. Мы использовали Node с Express для написания базовой рабочей реализации этого метода и рассмотрели, как использовать его на стороне клиента, используя Backbone в качестве примера. Код для этой статьи доступен на GitHub .

В спецификации есть еще кое-что, что мы не реализовали в полной мере, например, «заявки» на ресурсы, но мы использовали базовое предложение для создания механизма обмена учетными данными для токена доступа, в данном случае между клиентом. и сервер приложения JavaScript.

Конечно, вы можете применить этот подход к другим технологиям — например, к Ruby или PHP-бэкэнду, или к Ember или AngularJS-приложениям. Кроме того, вы можете принять его для мобильных приложений. Например, используя веб-технологии в сочетании с чем-то вроде PhoneGap, используя такой инструмент, как Sencha, или как полностью нативное приложение.