Вступление

Добро пожаловать снова! Если вы пришли к этому сообщению из ниоткуда и не понимаете, о чем все это, вы можете проверить предыдущие сообщения:

Вы можете найти код этого руководства в GitHub.

Если вы выполнили шаги из предыдущих статей, ваша структура каталогов проекта должна выглядеть так:

├── config
│   ├── env
│   │   ├── development.js
│   │   └── index.js
│   └── express.js
├── gulpfile.babel.js
├── index.js
├── package.json
└── server
    ├── controllers
    │   ├── tasks.js
    │   └── users.js
    ├── models
    │   ├── task.js
    │   └── user.js
    └── routes
        ├── index.js
        ├── tasks.js
        └── users.js

Если этого не произошло, вернитесь и проверьте, не пропустили ли вы что-нибудь из предыдущих сообщений.

Безопасность RESTful API

Пришло время заняться ключевым моментом в нашем RESTful API: безопасностью. У нас есть конечные точки для управления пользователями и задачами, но прямо сейчас любой клиент, который знает, где работает наш API, может получить доступ к конечным точкам. Это небезопасно, в реальном мире вам нужно контролировать, кто имеет доступ к вашему API, и вам нужно проверять каждый запрос, что он исходит из надежного источника.

Есть как минимум два аспекта безопасности, о которых вы хотите позаботиться при создании API: аутентификация и авторизация. Всегда найдется кто-то, кто не знает разницы между этими двумя, поэтому вот объяснение в одном предложении для обоих:

  • Аутентификация: действие по идентификации пользователя на вашей платформе.
  • Авторизация: действие проверки, имеет ли уже аутентифицированный пользователь доступ к определенному ресурсу.

Таким образом, вы можете рассматривать его как конвейер: запрос - ›аутентификация -› авторизация - ›ответ. Сначала вы проверяете, что запрос исходит из надежного источника, а затем проверяете, что у доверенного источника есть доступ к ресурсу, который он пытается получить из вашей системы.

В нашем API аутентификация будет обеспечиваться конечной точкой входа пользователя / пароля, которая вернет JWT (веб-токен JSON) - подробнее об этом позже - который можно использовать для аутентификации последующих запросов. Наши контроллеры будут просто обрабатывать авторизацию для выполнения некоторых базовых проверок, например запрета пользователю видеть или изменять задачи других пользователей.

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

Аутентификация API

Итак, давайте начнем с процесса аутентификации для нашего API. Сначала я перечислю этапы аутентификации, а потом мы рассмотрим более подробную информацию. В основном поток доступа к нашему API будет следующим:

  1. Клиент запрашивает JWT для аутентификации своих запросов: POST / token -d username = user -d password = pass
  2. Сервер API проверяет комбинацию имени пользователя и пароля и генерирует JWT для возврата. Если комбинация пользователя и пароля недействительна, будет возвращена ошибка 401 неавторизовано.
  3. Клиент где-то хранит JWT (например, в localStorage, если это веб-браузер).
  4. Клиент отправляет запросы на доступ к ресурсам, включая JWT в заголовке Авторизация запроса.
  5. API проверяет, что JWT действителен, и возвращает данные ресурса или 401 Неавторизованный, если он недействителен.

Так что пришло время развеять сомнения в том, что я уже упоминал несколько раз в этом посте.

JWT (веб-токен JSON)

Как сказано на сайте JWT.io:

Веб-токен JSON (JWT) - это открытый стандарт (RFC 7519), который определяет компактный и автономный способ безопасной передачи информации между сторонами в виде объекта JSON.

По сути, JWT - это объект JSON, закодированный в строке base 64, который содержит информацию об объекте (пользователе или клиенте API). Хорошо то, что эта строка компактна и может быть отправлена ​​как параметр POST запроса или даже в заголовке запроса (как мы будем его использовать).

На этом этапе вы можете спросить себя, как мы можем доверять закодированной строке, которую отправляет клиент. Что, если клиент создает закодированную строку с поддельными данными? Секрет в том, что JWT, который сервер API возвращает клиенту, имеет цифровую подпись с секретным ключом, известным только серверу, и использует алгоритм HMAC. Таким образом, сервер всегда будет проверять действительность подписи перед анализом содержимого JWT. Поэтому, если вы не дадите людям свой секретный ключ, никто без аутентификации по имени пользователя и паролю не сможет получить доступ к вашему API.

Строка в кодировке JWT разделена на 3 раздела: заголовок, полезная нагрузка и подпись. Более подробную информацию о разделах JWT вы можете найти на сайте JWT.io.

СТ в ОТДЫХЕ

Так почему же все эти штуки с JWT вместо того, чтобы выполнять какой-то логин в конечной точке и поддерживать сеанс активным в течение некоторого заранее определенного времени? Именно тогда в игру вступает ST-часть REST. Значение REST - «передача репрезентативного состояния». Передача состояния означает, что в каждом HTTP-запросе мы передаем состояние, нам не нужно сохранять состояние на каком-либо сервере. Таким образом, статус сеанса всегда поддерживается клиентами, и именно клиенты отправляют информацию о сеансе на сервер с каждым запросом.

Это дает много преимуществ, но особенно важно для масштабирования нашего API. Отсутствие данных сеанса на наших серверах API позволяет нам иметь несколько экземпляров для обслуживания любого количества клиентов, и любой экземпляр может обслуживать любого клиента в любое время. На сервере нет данных сеанса, а это означает, что разные серверы могут обрабатывать последовательные запросы от одного и того же клиента. Поскольку клиент отправляет данные сеанса в запросе (JWT), каждый сервер может проверять, является ли он действительным клиентом, с каждым запросом. Очень хорошее объяснение можно найти в ответе Джаррод Роберсон о переполнении стека this.

Реализация JWT

Хорошо, теперь, когда мы понимаем всю эту штуку с JWT, давайте посмотрим, как мы можем реализовать ее в нашем API, чтобы сделать ее более безопасной. Нам потребуется установить модуль jsonwebtoken, чтобы сгенерировать JWT для допустимых пользователей. Нам также необходимо установить модуль express-jwt, который предоставляет полезное промежуточное программное обеспечение, которое будет проверять, действителен ли JWT, отправленный с запросами, или нет.

npm install --save jsonwebtoken@^7.0.1 express-jwt@^3.4.0

Контроллер аутентификации

Давайте сначала создадим функции, которые будут обрабатывать первоначальную аутентификацию пользователя. Это шаги 1 и 2 потока аутентификации. Перейдите в каталог server / controllers и создайте файл auth.js. Мы организуем наш процесс аутентификации из трех этапов, каждый из которых будет реализован функцией контроллера:

  1. Authenticate: проверит правильность имени пользователя и пароля.
  2. generateToken: будет генерировать JWT.
  3. responseJWT: отправит JWT обратно клиенту.

Для шага 1 добавьте следующий код в только что созданный файл server / controllers / auth.js:

import User from '../models/user';

function authenticate(req, res, next) {
  User.findOne({
      username: req.body.username
    })
    .exec()
    .then((user) => {
      if (!user) return next();
      user.comparePassword(req.body.password, (e, isMatch) => {
        if (e) return next(e);
        if (isMatch) {
          req.user = user;
          next();
        } else {
          return next();
        }
      });
    }, (e) => next(e))
}

Здесь мы просто сначала проверяем, существует ли пользователь с указанным req.body.username. Если он существует, мы затем проверяем, является ли указанный req.body.password правильным, используя метод экземпляра comparePassword, который мы создали в предыдущем посте «База данных: Настраивать"". Если пароль правильный, мы вызываем следующее промежуточное ПО для маршрута.

Теперь давайте добавим код для создания JWT для аутентифицированного пользователя. Чтобы создать JWT, нам нужно будет определить секретный ключ, который мы будем использовать для подписи JWT и распознать позже, если JWT, отправленный клиентом, не был изменен. Поэтому нам нужно создать новое свойство конфигурации в нашем файле config / env / development.js с именем jwtSecret. И мы также добавим еще одно свойство конфигурации для продолжительности JWT:

export default {
  env: 'development',
  db: 'mongodb://localhost/node-es6-api-dev',
  port: 3000,
  jwtSecret: 'my-api-secret',
  jwtDuration: '2 hours'
};

После этого мы готовы добавить код для генерации JWT в наш контроллер аутентификации:

import jwt from 'jsonwebtoken';
import config from '../../config/env';
import User from '../models/user';

...

function generateToken(req, res, next) {
  if (!req.user) return next();

  const jwtPayload = {
    id: req.user._id
  };
  const jwtData = {
    expiresIn: config.jwtDuration,
  };
  const secret = config.jwtSecret;
  req.token = jwt.sign(jwtPayload, secret, jwtData);

  next();
}

Обратите внимание на два добавленных импорта вверху. В этой функции мы определяем полезную нагрузку JWT, которая будет включать только свойство пользователя _id, которого достаточно, чтобы наш сервер мог идентифицировать пользователя. Затем мы устанавливаем дату истечения срока действия токена и, наконец, подписываем его, используя секрет, определенный в нашем файле конфигурации. После подписания мы передаем управление следующему промежуточному программному обеспечению, которое будет отправлять ответ обратно. Итак, давайте добавим код для последнего промежуточного программного обеспечения аутентификации:

function respondJWT(req, res) {
  if (!req.user) {
    res.status(401).json({
      error: 'Unauthorized'
    });
  } else {
    res.status(200).json({
      jwt: req.token
    });
  }
}

export default { authenticate, generateToken, respondJWT };

Обратите внимание, что я также добавил оператор экспорта в конец контроллера.

Все настроено на стороне контроллера, давайте настроим маршрут аутентификации.

Маршрут Auth

Создайте новый файл auth.js в каталоге server / routes и поместите в него следующее содержимое:

import express from 'express';
import authCtrl from '../controllers/auth';

const router = express.Router();

router.route('/token')
  /** POST /api/auth/token Get JWT authentication token */
  .post(authCtrl.authenticate,
    authCtrl.generateToken,
    authCtrl.respondJWT);

export default router;

Теперь давайте подключим определение маршрута к пути / auth в нашем API, добавив в файл server / routes / index.js следующие строки:

import authRoutes from './auth';

router.use('/auth', authRoutes);

Все готово! Мы готовы проверить, может ли наш API сгенерировать JWT и вернуть его клиенту. Зайдите в консоль и попробуйте:

curl http://localhost:3000/api/auth/token -d username=mauricio -d password=mauricio

Ответ должен выглядеть так:

{ "jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjU3OTlhNT..." }

А если вы отправите неправильный пароль или имя пользователя, вы получите следующее:

{ "error": "Unauthorized" }

Вы даже можете зайти в JWT.io Debugger и вставить JWT, который ваш API возвращает, чтобы проверить, правильно ли он. И вы также можете попробовать поместить jwtSecret из файла конфигурации в отладчик, чтобы проверить, действительна ли подпись.

Проверка входящих JWT

До сих пор мы предоставляли нашим клиентам JWT, который они могут использовать в последующих запросах, но мы не добавляли никакого кода для проверки того, является ли отправляемый клиентом JWT действительным или нет. Здесь мы будем использовать модуль express-jwt, который мы установили некоторое время назад.

Перейдите в каталог config и создайте там файл jwt.js со следующим содержимым:

import config from './env';
import jwt from 'express-jwt';

const authenticate = jwt({
  secret: config.jwtSecret
});

export default authenticate;

Мы экспортируем функцию jwt, которая представляет собой не что иное, как промежуточное программное обеспечение, которое проверяет заголовок авторизации входящих запросов. По умолчанию он проверяет значения, равные JWT [JWT_STRING], поэтому допустимый заголовок будет, например, таким:

Authorization: JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjU3OTlhNTcyMjg0ZD...

Теперь давайте воспользуемся этим промежуточным программным обеспечением в нашей конфигурации маршрутов, чтобы добавить уровень аутентификации. Я буду использовать server / routes / users.js в качестве примера. Позже вы можете добавить аутентификацию для любой конечной точки, которую хотите ограничить только зарегистрированными клиентами.

import express from 'express';
import userCtrl from '../controllers/users';
import auth from '../../config/jwt';

const router = express.Router();

router.route('/')
  /** GET /api/users - Get list of users */
  .get(auth, userCtrl.list);

Вот и все! Просто добавив промежуточное ПО для аутентификации в начало списка промежуточного ПО для маршрутизации, все готово. Теперь попробуйте получить доступ к конечной точке списка пользователей без отправки заголовка авторизации, и вы получите такой ответ:

UnauthorizedError: No authorization token was found<br> &nbsp; &nbsp;at middleware ....

Хорошо, что мы защитили нашу конечную точку, но этот ответ выглядит не очень хорошо, правда? Давайте все исправим. Перейдите в файл config / express.js и добавьте следующее промежуточное ПО для обработки ошибок после оператора app.use (‘/ api’, routes);:

app.use((err, req, res, next) => {
  res.status(err.status)
    .json({
      status: err.status,
      message: err.message
    });
});

Это позаботится обо всех ошибках, вызванных вызовами next (err) в наших функциях контроллера. Теперь попробуйте еще раз получить доступ к конечной точке списка пользователей без JWT, ответ будет выглядеть следующим образом:

{ "status": 401, "message": "No authorization token was found" }

Лучше правда? Итак, теперь вы можете добавить промежуточное ПО auth к любому маршруту, который хотите защитить от неавторизованных пользователей.

Далее…

Наши конечные точки защищены бессессионным подходом с помощью JWT. Мы почти закончили, последние две вещи, которые нам нужно сделать, - это добавить проверку данных на наши конечные точки и добавить некоторое модульное тестирование с использованием мокко. Чтобы продолжить валидацию, перейдите к следующему посту!

Не пропустите наши публикации, подпишитесь на нас прямо сейчас в Twitter! 👇