Аутентификация с помощью JSON Web Token
Во время разработки RESTful API для одного проекта возникла необходимость реализовать аутентификацию пользователя. Одним из принципов REST является независимость от состояния (stateless). Это значит, что клиент должен сам позаботиться о своей аутентификации при каждом запросе. Поискав в интернете статьи на эту тему, я обнаружил интересную технологию — JSON Web Token или просто JWT.
Привычные подходы
Самый простой подход для аутентификации в REST это отправка логина и пароля пользователя при каждом запросе. Понятно, что такой способ не безопасен, особенно если клиент использует незащищенный протокол.
Более привычное решение — сопоставление пользователя некому уникальному идентификатору — токену. При первом логине клиенту от сервера выдается токен, образованный хеш-функцией от каких-нибудь уникальных данных пользователя (id, логин, пароль). В базу заносится пара токен — id. При следующих запросах клиент передает этот токен, а сервер ищет в базе запись. Если запись найдена, пользователя авторизуют. Часто, для большей безопасности, токену дают определенное время жизни, после которого он становится недействителен.
JWT
JSON Web Token работает схоже с привычной реализацией. Но JWT имеет некоторые преимущества — он самодостаточен, все необходимые для аутентификации данные можно хранить в самом токене. Последовательно рассмотрим устройство токена.
Структура
JWT состоит из трех основных частей: заголовка (header), нагрузки (payload) и подписи (signature). Заголовок и нагрузка формируются отдельно, а затем на их основе вычисляется подпись.
Header
Обычно заголовок состоит из двух полей: типа токена (в данном случае JWT) и алгоритма хэширования подписи:
Официальный сайт jwt.io предлагает два алгоритма хэширования: HS256 и RS256. Но на деле можно использовать любой алгоритм с приватным ключом.
Payload
Payload — это любые данные, которые вы хотите передать в токене. Но стандарт предусматривает несколько зарезервированных полей:
- iss — (issuer) издатель токена
- sub — (subject) «тема», назначение токена
- aud — (audience) аудитория, получатели токена
- exp — (expire time) срок действия токена
- nbf — (not before) срок, до которого токен не действителен
- iat — (issued at) время создания токена
- jti — (JWT id) идентификатор токена
Все эти поля не являются обязательными, но их использование не по назначению может привести к коллизиям.
Любые другие данные можно передавать по договоренности между сторонами, использующими токен. Например, payload может выглядеть так:
Payload не шифруется при использовании токена, поэтому не стоит передавать в нем данные, которые не должны попасть в открытый доступ.
Signature
Подпись вычисляется на основе заголовка и нагрузки. Таким образом, если кто-то попытается изменить данные в токене, он не сможет изменить подпись, не зная приватного ключа. При аутентификации приватным ключом может выступать пароль пользователя (или хеш от пароля).
Сначала header и payload приводятся к формату JSON, а затем переводятся в base64:
Header: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 Payload: eyJpc3MiOiJDb2RleCBUZWFtIiwic3ViIjoiYXV0aCIsImV4cCI6MTUwNTQ2Nzc1Njg2OSwiaWF0IjoxNTA1NDY3MTUyMDY5LCJ1c2VyIjoxfQ
Затем, две эти строки соединяются через точку и хэшируются указанным в header алгоритмом. Допустим, пользователь использует пароль password:
HS256(‘eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9’ + ‘.’ + ‘eyJpc3MiOiJDb2RleCBUZWFtIiwic3ViIjoiYXV0aCIsImV4cCI6MTUwNTQ2Nzc1Njg2OSwiaWF0IjoxNTA1NDY3MTUyMDY5LCJ1c2VyIjoxfQ’, ‘password’) = ‘0ynjTRZT9Uk77TnGy_g9Mxi1decLBjKxQK6e2dVzDJo’
Результат работы алгоритма и есть подпись. Теперь осталось только сформировать сам токен, для этого нужно через точку соединить header и payload в base64 и подпись:
Токен готов. Проверить его можно на jwt.io.
Аутентификация
После первого логина, клиенту возвращается сгенерированный сервером JWT. При каждом следующем запросе, клиент должен передавать JWT установленным API способом (например, через заголовок или как параметр запроса). Сервер декодирует header и payload и проверяет зарезервированные поля. Если все в порядке, по указанному в header алгоритму составляется подпись. Если полученная подпись совпадает с переданной, пользователя авторизуют. Можно реализовать всю эту схему вручную, а можно использовать одну из библиотек указанных на jwt.io.
If you like this article, share a link with your friends
Read more
We talk about interesting technologies and share our experience of using them.
Реализация JWT в Spring Boot

Теперь, когда мы знаем что такое JWT токен и как он устроен, пришло время использовать наши теоретические знания на практике.
Одно из преимуществ аутентификации с использованием JWT – это возможность выделить выдачу токенов и работу с данными пользователей в отдельное приложение, переложив механизм валидации токенов на клиентские приложения. Этот механизм отлично подходит микросервисной архитектуре.
Сначала сделаем приложение, которое будет совмещать в себе бизнес-логику и выдачу токенов. А чуть позже мы сделаем два приложения: одно будет отвечать за выдачу токенов, а другое содержать бизнес логику. Сервис с бизнес логикой не сможет выдавать токены, но сможет их валидировать. Таких приложений может быть много.
Давайте рассмотрим механизм аутентификации по шагам. Обычно в обучающих статьях опускают наличие refresh токена. Однако, refresh токен является важной частью в аутентификации с помощью JWT, поэтому мы рассмотрим и его. Вот как будет выглядеть механизм аутентификации:
- Клиент API, чаще всего это фронт, присылает нам объект с логином и паролем.
- Если пароль подходит, то мы генерируем access и refresh токены и отправляем их в ответ.
- Клиент API использует access токен для взаимодействия с остальным нашим эндпойнтами нашего API.
- Через пять минут, когда access токен протухает, фронт отправляет refresh токен и получает новый access токен. И так по кругу, пока не протухнет refresh токен.
- Refresh токен выдается на 30 дней. Примерно на 25-29 день клиент API отправляет валидный refresh токен вместе с валидным access токеном и взамен получает новую пару токенов.
В этой статье будет чистый REST-сервис без фронтенда. Подразумевается, что front написан отдельно.
Спонсор поста
Используемые версии
Java: 17
SpringBoot: 2.7.0
jsonwebtoken: 0.11.5
jaxb-api: 2.3.1
История изменений
21.06.2022: Актуализировал версию спрингбута и перевел проект на 17 Java. Удалил все устаревшие классы и методы, заменив их на аналогичные современные варианты. Детализировал некоторые моменты в статье.
Создание сервера аутентификации
Проект на GitHub: jwt-server-spring
Если вы хотите повторять все действия из статьи, чтобы лучше понять что происходит. То для вас я собрал начальную конфигурацию приложения на сайте start.spring.io, вам остается только скачать ее, распаковать, и открыть с помощью IDEA.
После этого в pom.xml добавим зависимости. Они понадобятся нам для генерации JWT токенов.

После этого берем access токен и вcтавляем его на вкладке Authorization, выбрав тип «Bearer token».

Далее выполняем запрос и получаем заветный результат.

Также попробуем получить сообщение предназначенное только для админов.

Видим 403 ошибку доступа, все работает. Если подождать 5 минут, то и по эндпойнту /api/hello/user увидим ту же ошибку, так как access токен протух.
Чтобы получить новый access токен отправляем запрос на /api/auth/token . В теле запроса указываем наш refresh токен.

Все работает. А теперь взамен текущего refresh токена получим новые access и refresh токены. Для этого вызовем /api/auth/refresh и передадим наш текущий refresh токен в теле запроса. Также не стоит забывать, что это защищенный метод, поэтому во вкладке Authorization вставляем наш access токен.

Отлично, мы получили новую пару токенов. А что если попробовать снова вызвать этот же запрос?

Мы получим ошибку, так как этого токена больше нет в сохраненных.
На этом основная часть закончена. Мы реализовали JWT аутентификацию. Теперь вы запросто реализуете приложение клиент, если это необходимо. Нужно только оставить функционал проверки токенов и аутентификации, и убрать функционал по выдаче новых токенов. Для удобства я сделал одно небольшое приложение клиент для демонстрации.
Склонируйте его себе и запустите. Токены выдает наш сервер аутентификации, а приложение клиент может их использовать для доступа к своему API.
Резюмирую
В этой статье мы реализовали аутентифицкаицю в SpringBoot сервисе с использованием JWT.
Теперь вы знаете, что можно выдавать токены в одном сервисе, а в других сервисах их валидировать, тем самым уменьшая запросы к сервису аутентификации. Такой подход можно использовать в микросервисной архитектуре.
Как сгенерировать 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.
Сгенерированный JWT отправляется клиенту, где он сохраняется в localStorage или sessionStorage , и будет отправляться клиентом серверу при каждом HTTP запросе в заголовке Authorization .
Authorization: Bearer
Сервер в свою очередь при обращении к маршрутам, требующих авторизации, извлекает данные из токена и проверяет валидность токена и наличие указанного в JWT пользователя.
Рассмотрим пример использования в Node.js JWT.
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" > ]
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 .
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>`) );
Токен авторизации на примере JSON WEB Token

Доброго времени суток, дорогой читатель. В данной статье я постараюсь рассказать об одном из самых популярных (на сегодняшний день) способов авторизации в различных клиент-серверных приложениях — токен авторизации. А рассматривать мы его будем на примере самой популярной реализации — JSON Web Token или JWT.
Введение
Начнем с того, что важно уметь различать следующие два понятия: аутентификации и авторизации. Именно с помощью этих терминов почти все клиент-серверные приложения основывают разделение прав доступа в своих сервисах.
Очень часто к процессу аутентификации примешивают и процесс индентификации — процесс, позволяющий определить что за пользователь в данный момент хочет пользоваться нашим приложением, например, путем ввода логина. Далее нам, как разработчикам и как ответственным людям хочется убедиться, что данный пользователь действительно тот за кого он себя выдает — и поэтому следует процесс аутентификации, когда пользователь подтверждает, что он тот самый %user_name%, например, путем ввода пароля.
Казалось бы, все что необходимо выполнить для безопасности нашего приложения мы сделали. Но нет, еще одним очень важным шагом в любом клиент-серверном приложении является разграничение прав, разрешение или запрет тех или иных действий данному конкретному аутентифицированному пользователю — процесс авторизации.
Еще раз кратко закрепим: сначала идет идентификация и аутентификация, процессы когда мы определяем и удостоверяемся, что за пользователь в данный момент использует наше приложение, а далее идет авторизация — процесс принятия решения о разрешенных данному пользователю действиях.
Еще одно небольшое введение
Прежде чем начать говорить о самом токене авторизации следует упомянуть для каких целей вообще его решили использовать. Поскольку мы знаем, что почти весь интернет так или иначе построен на протоколе HTTP(или его старшем брате HTTPS) и что он не отслеживает состояние, то есть при каждом запросе HTTP ничего не знает, что происходило до этого, он лишь передает запросы, то возникает следующая проблема: если аутентификация нашего пользователя происходит с помощью логина и пароля, то при любом следующем запросе наше приложение не будет знать все тот же ли этот человек, и поэтому придётся каждый раз заново логиниться. Решением данной проблемы является как раз наш токен, а конкретно его самая популярная реализация — JSON Web Tokens (JWT). Также помимо решения вопросов с аутентификацией токен решает и другую не менее важную проблему авторизации (разграничение разрешенных данному пользователю действий), о том каким образом мы узнаем ниже, когда начнем разбирать структуру токена.
Формальное определение
Приступим наконец к работе самого токена. Как я сказал ранее в качестве токенов наиболее часто рассматривают JSON Web Tokens (JWT) и хотя реализации бывают разные, но токены JWT превратились в некий стандарт, именно поэтому будем рассматривать именно на его примере.
JSON Web Token (JWT) — это открытый стандарт (RFC 7519) для создания токенов доступа, основанный на формате JSON.
Фактически это просто строка символов (закодированная и подписанная определенными алгоритмами) с некоторой структурой, содержащая полезные данные пользователя, например ID, имя, уровень доступа и так далее. И эта строчка передается клиентом приложению при каждом запросе, когда есть необходимость идентифицировать и понять кто прислал этот запрос.
Принцип работы
Рассмотрим принцип работы клиент серверных приложений, работающих с помощью JWT. Первым делом пользователь проходит аутентификацию, конечно же если не делал этого ранее и в этом есть необходимость, а именно, например, вводит свой логин и пароль. Далее приложение выдаст ему 2 токена: access token и refresh token (для чего нужен второй мы обсудим ниже, сейчас речь идет именно об access token). Пользователь тем или иным способом сохраняет его себе, например, в локальном хранилище или в хранилище сессий. Затем, когда пользователь делает запрос к API приложения он добавляет полученный ранее access token. И наконец наше приложение, получив данный запрос с токеном, проверяет что данный токен действительный (об этой проверке, опять же, ниже), вычитывает полезные данные, которые помогут идентифицировать пользователя и проверить, что он имеет право на запрашиваемые ресурсы. Таким нехитрым образом происходит основная логика работы с JSON Web Tokens.

Структура токена
Пришло время обсудить структуру токена и тем самым лучше разобраться в его работе. Первое что следует отметить, что JWT токен состоит из трех частей, разделенных через точку:
- Заголовок (header)
- Полезные данные (playload)
- Подпись (signature)

Рассмотрим каждую часть по подробнее.
Заголовок
Это первая часть токена. Она служит прежде всего для хранения информации о токене, которая должна рассказать о том, как нам прочитать дальнейшие данные, передаваемые JWT. Заголовок представлен в виде JSON объекта, закодированного в Base64-URL Например:
Если раскодировать данную строку получим:
Заголовок содержит два главных поля: alg и typ. Поле typ служит для информации о типе токена, но как я уже упоминал ранее, что JWT превратился в некий стандарт, то это поле перестало нести особый смысл и служит скорее для целей будущего, если вдруг появится улучшенная версия алгоритма JWT(2.0), которая заменит JWT. Поле alg задает алгоритм шифрования. Обязательный для поддержки всеми реализациями является алгоритм HMAC с использованием SHA-256, или же, как он обозначен в заголовке, HS256. Для работы с этим алгоритмом нужен один секретный ключ, конкретный механизм работы рассмотрим ниже. Для справки можно также отметить, что существует и асимметричный алгоритм, который можно использовать в JWT, например, RS256. Для работы с ним требуется два ключа — открытый и закрытый. Но в данной статье рассмотрим работу с одним закрытым ключом.
Полезные данные
Перейдем наконец к полезным данным. Опять же — это JSON объект, который для удобства и безопасности передачи представляется строкой, закодированной в base64. Наглядный пример полезных данных (playload) токена может быть представлен следующей строкой:
Что в JSON формате представляет собой:
Именно здесь хранится вся полезная информация. Для данной части нет обязательных полей, из наиболее часто встречаемых можно отметить следующие:
iss — используется для указания приложения, из которого отправляется токен.
user_id — для идентификации пользователя в нашем приложении, кому принадлежит токен.
Одной из самых важных характеристик любого токена является время его жизни, которое может быть задано полем exp. По нему происходит проверка, актуален ли токен еще (что происходит, когда токен перестает быть актуальным можно узнать ниже). Как я уже упоминал, токен может помочь с проблемой авторизации, именно в полезных данных мы можем добавить свои поля, которые будут отражать возможности взаимодействия пользователя с нашим приложением. Например, мы можем добавить поле is_admin или же is_preferUser, где можем указать имеет ли пользователь права на те или иные действия, и при каждом новом запросе с легкостью проверять, не противоречат ли запрашиваемые действия с разрешенными. Ну а что же делать, если попробовать изменить токен и указать, например, что мы являемся администраторами, хотя таковыми никогда не были. Здесь мы плавно можем перейти к третьей и заключительной части нашего JWT.
Подпись
На данный момент мы поняли, что пока токен никак не защищен и не зашифрован, и любой может изменить его и тем самым нарушается вообще весь смысл аутентификации. Эту проблему призвана решить последняя часть токена — а именно сигнатура (подпись). Происходит следующее: наше приложение при прохождении пользователем процедуры подтверждения, что он тот за кого себя выдает, генерирует этот самый токен, определяет поля, которые нужны, записывает туда данные, которые характеризуют данного пользователя, а дальше с помощью заранее выбранного алгоритма (который отмечается в заголовке в поле alg токена), например HMAC-SHA256, и с помощью своего приватного ключа (или некой секретной фразы, которая находится только на серверах приложения) все данные токена подписываются. И затем сформированная подпись добавляется, также в формате base64, в конец токена. Таким образом наш итоговый токен представляет собой закодированную и подписанную строку. И далее при каждом новом запросе к API нашего приложения, сервер с помощью своего секретного ключа сможет проверить эту подпись и тем самым убедиться, что токен не был изменен. Эта проверка представляет собой похожую на подпись операцию, а именно, получив токен при новом запросе, он вынимает заголовок и полезные данные, затем подписывает их своим секретным ключом, и затем идет просто сравнение двух получившихся строк. Таким нехитрым способом, если не скомпроментировать секретный ключ, мы всегда можем знать, что перед нами все еще наш %user_name% с четко отведенными ему правами.
Время жизни токена и Refresh Token
Теперь плавно перейдем к следующему вопросу — времени жизни токена, и сопутствующей этой теме refresh token. Мы помним, что одно из важнейших свойств токена — это время его жизни. И оно совсем недолговечное, а именно 10-30 минут. Может возникнуть вопрос: а зачем такое короткое время жизни, ведь тогда придется каждый раз заново создавать новый токен, а это лишняя нагрузка на приложения. А ответ достаточно очевидный, который включает в себя и одновременно ответ на вопрос: а что делать если токен был перехвачен. Действительно, если токен был перехвачен, то это большая беда, так как злоумышленник получает доступ к приложению от имени нашего %user_name%, но так как access token является короткоживущим, то это происходит лишь на недолгий период. А дальше этот токен уже является не валидным. И именно чтобы обновить и получить новый access token нужен refresh token. Как мы знаем (или если забыли можем снова прочитать в начале) пользователь после процесса аутентификацию получает оба этих токена. И теперь по истечении времени жизни access token мы отсылаем в приложение refresh token и в ответ получаем снова два новых токена, опять же один многоразовый, но ограниченный по времени — токен доступа, а второй одноразовый, но долгоживущий — токен обновления. Время жизни refresh token вполне может измеряться месяцами, что достаточно для активного пользователя, но в случае если и этот токен окажется не валидным, то пользователю следует заново пройти идентификацию и аутентификацию, и он снова получит два токена. И весь механизм работы повторится.
Заключение
В данной статье я постарался подробно рассмотреть работу клиент-серверных приложений с токеном доступа, а конкретно на примере JSON Web Token (JWT). Еще раз хочется отметить с какой сравнительной легкостью, но в тоже время хорошей надежностью, токен позволяет решать проблемы аутентификации и авторизации, что и сделало его таким популярным. Спасибо за уделенное время.
Полезные ссылки
- 5 Easy Steps to Understanding JSON Web Tokens (JWT)
- JWT — как безопасный способ аутентификации и передачи данных
- Securing React Redux Apps With JWT Tokens
- Зачем нужен Refresh Token, если есть Access Token?