Перейти к содержанию

JWT

JSON Web Token, или просто JWT, представляет собой строку, полученную на основе формата JSON, и используется в качестве более безопасной и простой альтернативы сессиям и файлам cookie для авторизации.

JWT позволяет уйти от хранения данных авторизованного пользователя на сервере и возлагает на сервер только задачу по верификации подписи.

JWT формируют три части:

  • заголовок (header);
  • данные (payload);
  • подпись (signature).

Заголовок представляет собой объект JSON и описывает сам токен с помощью следующих свойств:

  • alg - алгоритм шифрования, используемый для подписи JWT, если токен не подписывается, то значением должно быть none (обязательный параметр);
  • typ - тип токена, необходимо указывать со значением "JWT", если могут использоваться токены другого типа (необязательный параметр);
  • ctp - тип данных, необходимо указывать со значением "JWT", если в payload присутствуют пользовательские ключи.

В данных, которые также передаются объектом JSON, указывается необходимая информация о пользователе. Также возможно задание значений предопределенных ключей (все они не обязательны) для описания конфигурации токена:

  • iss - приложение, создавшее токен;
  • sub - назначение JWT;
  • aud - массив получателей токена;
  • exp - дата и время, указанное в миллисекундах, прошедших с 01.01.1970, до наступления которого JWT будет валиден;
  • nbf - дата и время, указанное в миллисекундах, прошедших с 01.01.1970, до наступления которого JWT будет не валиден;
  • iat - дата и время создания JWT, указанное в миллисекундах, прошедших с 01.01.1970;
  • jti - уникальный идентификатор токена.

Заголовок и данные используются для вычисления значения подписи по указанному в заголовке в свойстве alg алгоритму шифрования.

Далее формируется сам JWT.

`${base64url(header)}.${base64url(payload)}.${signature}`

Сгенерированный JWT отправляется клиенту, где он сохраняется в localStorage или sessionStorage, и будет отправляться клиентом серверу при каждом HTTP запросе в заголовке Authorization.

Authorization: Bearer {token}

Сервер в свою очередь при обращении к маршрутам, требующих авторизации, извлекает данные из токена и проверяет валидность токена и наличие указанного в JWT пользователя.

Рассмотрим пример использования в Node.js JWT.

users.json

[
  {
    "id": 1,
    "login": "user1",
    "password": "password1"
  },
  {
    "id": 2,
    "login": "user2",
    "password": "password2"
  },
  {
    "id": 3,
    "login": "user3",
    "password": "password3"
  }
]

app.js

const express = require('express'),
  app = express(),
  bodyParser = require('body-parser'),
  crypto = require('crypto'),
  users = require('./users')

const host = '127.0.0.1'
const port = 7000

const tokenKey = '1a2b-3c4d-5e6f-7g8h'

app.use(bodyParser.json())
app.use((req, res, next) => {
  if (req.headers.authorization) {
    let tokenParts = req.headers.authorization.split(' ')[1].split('.')
    let signature = crypto
      .createHmac('SHA256', tokenKey)
      .update(`${tokenParts[0]}.${tokenParts[1]}`)
      .digest('base64')

    if (signature === tokenParts[2]) req.user = JSON.parse(Buffer.from(tokenParts[1], 'base64').toString('utf8'))

    next()
  }

  next()
})

app.post('/api/auth', (req, res) => {
  for (let user of users) {
    if (req.body.login === user.login && req.body.password === user.password) {
      let head = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'jwt' })).toString('base64')
      let body = Buffer.from(JSON.stringify(user)).toString('base64')
      let signature = crypto
        .createHmac('SHA256', tokenKey)
        .update(`${head}.${body}`)
        .digest('base64')

      return res.status(200).json({
        id: user.id,
        login: user.login,
        token: `${head}.${body}.${signature}`
      })
    }
  }

  return res.status(404).json({ message: 'User not found' })
})

app.get('/user', (req, res) => {
  if (req.user) return res.status(200).json(req.user)
  else return res.status(401).json({ message: 'Not authorized' })
})

app.listen(port, host, () => console.log(`Server listens http://${host}:${port}`))

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

Для экономии времени и избежания реализации собственного алгоритма формирования в Node.js JWT можно использовать npm модуль jsonwebtoken.

app.js

const express = require('express'),
  app = express(),
  bodyParser = require('body-parser'),
  jwt = require('jsonwebtoken'),
  users = require('./users')

const host = '127.0.0.1'
const port = 7000

const tokenKey = '1a2b-3c4d-5e6f-7g8h'

app.use(bodyParser.json())
app.use((req, res, next) => {
  if (req.headers.authorization) {
    jwt.verify(req.headers.authorization.split(' ')[1], tokenKey, (err, payload) => {
      if (err) next()
      else if (payload) {
        for (let user of users) {
          if (user.id === payload.id) {
            req.user = user
            next()
          }
        }

        if (!req.user) next()
      }
    })
  }

  next()
})

app.post('/api/auth', (req, res) => {
  for (let user of users) {
    if (req.body.login === user.login && req.body.password === user.password) {
      return res.status(200).json({
        id: user.id,
        login: user.login,
        token: jwt.sign({ id: user.id }, tokenKey)
      })
    }
  }

  return res.status(404).json({ message: 'User not found' })
})

app.get('/user', (req, res) => {
  if (req.user) return res.status(200).json(req.user)
  else return res.status(401).json({ message: 'Not authorized' })
})

app.listen(port, host, () => console.log(`Server listens http://${host}:${port}`))