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

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.

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

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

1
Authorization: Bearer {token}

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

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

users.json

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
[
    {
        "id": 1,
        "login": "user1",
        "password": "password1"
    },
    {
        "id": 2,
        "login": "user2",
        "password": "password2"
    },
    {
        "id": 3,
        "login": "user3",
        "password": "password3"
    }
]

app.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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}`)
);