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

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(),
  crypto = require('crypto'),
  users = require('./users')

const host = '127.0.0.1'
const port = 7000

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

app.use(express.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(),
  jwt = require('jsonwebtoken'),
  users = require('./users')

const host = '127.0.0.1'
const port = 7000

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

app.use(express.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}`)
)

Комментарии