Доброе время!
Часть 2-я по открытому занятию нового учебного курса: реализация простого JWT через новый Spring Boot OAuth2 Resource Server (первая часть: Spring Boot 3.0 — готовимся заранее). Что такое JWT и зачем, писать здесь не буду - в сети материалов много, начинать знакомство обычно рекомендую с Википедии. А вот хорошая ссылка по реализации JWT+OAuth2. Здесь я привожу Java код, основанный на официальном примере spring-projects - простейшей реализации JWT Login Sample (без refresh token и отдельного авторизационного сервера), "творчески доработанный" и с моими пояснениями. Еще раз - без теории, для тех, кому интересен код актуальной Java реализации. Если это Вы - прошу к прочтению.
Чтобы не повторять каждый раз основы, курс стартует с финального кода открытого проекта Spring Boot 2.x + HATEOAS: Basic аутентификация и авторизация на основе ролей, регистрация пользователя в приложении, управление своим профилем и администрирование пользователей (код на GitHub). Реализация добавления JWT аутентификации находится в ветке patched другого репозитория, начиная с 3го комита. Для разбора кода проще всего вычекать его к себе:
git clone --branch patched https://github.com/JavaOPs/cloudjava
Комит 1_03_jwt_token: реализуем получение JWT токена
Для большей безопасности будем использовать ассиметричные RS256 ключи: проверить подлинность токена может любой, а подписать его только тот, у кого есть приватный ключ. Ключи можно взять мои или заменить их своими (generate PKCS#8 private key with openssl):
openssl genpkey -out jwt.pem -algorithm RSA -pkeyopt rsa_keygen_bits:2048 - приватный ключ в формате PKCS8писи
openssl rsa -in jwt.pem -pubout -out jwt.pub - публичный ключ для декодирования
Поместим ключи в
\src\main\resources\keys
Добавляем зависимость
org.springframework.boot:spring-boot-starter-oauth2-resource-server
Объявляем ключи в
application.yaml
и инжектим вJwtSecurityConfiguration
. С помощью ключей создаем биныJwtDecoder/JwtEncoder
SecurityConfiguration
переименовываем вLoginSecurityConfiguration
.httpBasic
будем использовать только для получения JWTСоздаем
TokenController
, где генерируем JWT. Детали залогиненного пользователяAuthUser
добавляю в JWT в отдельномJwtUtil#addUserDetails
"Выключаем" тесты через Junit5
@Disabled
Перед сборкой не забываем gradle clean. Проверяем получение JWT:
### JetBrains Tools->HTTP Client
POST http://localhost:8080/token
Authorization: Basic admin@javaops.ru admin
В ответ получаем длинную строке типа eyJhbGciOiJSUzI1NiJ9..
.Она хорошо копируется из JetBrains HTTP Client. В Unix можно сделать export
, который можно использовать после следующего комита:
export JWT=`curl -XPOST -u admin@javaops.ru:admin localhost:8080/token`
1_04_jwt_api: переводим API на JWT:
Добавляем
JwtUser
, которого будем получать после JWT аутентификации изJwtUtil#createJwtUser
. Функционал по добавлению деталей пользователя в JWT и извлечению их должен находиться рядом.Добавляем в
JwtSecurityConfiguration
вторую конфигурацию AAA по JWT (порядок фильтров не важен). Конвертируем стандартныйJwtAuthenticationToken
вJwtUser
.В методы
ProfileController
инжектим наш principal objectJwtUser
(с@AuthenticationPrincipal
у меня не работает). В случае, когда для функционала необходим пользователь, достаем его из базы:AbstractUserController#findByJwtUser
(не обязательно это делать для всех методов API).В SecurityContext при обращении к API теперь попадает объект
JwtUser
, рефакторимSecurityUtil
gradle clean и проверяем API: полученный JWT вставляем вместо [JWT_TOKEN]:
GET http://localhost:8080/api/profile
Authorization: Bearer [JWT_TOKEN]
или
curl -H "Authorization: Bearer $JWT" localhost:8080/api/profile
1_05_jwt_test: восстанавливаем тесты
Как вариант можно воспользоваться jwt() подменой. Но я делаю все честно, с получением токена:
В
AbstractControllerTest
добавляем вспомогательные методыgetJWT
с Basic авторизациейДобавляем тесты на получения JWT:
TokenControllerTest
Для API добавляем вспомогательный
AbstractControllerTest#performJwt
с аторизационным Bearer заголовком. Чтобы токены не перезапрашивать в каждом тесте, кэшируем их вAbstractControllerTest#userJwtMap
(синхронизация не нужна - не проблема, если даже они перевычислятся несколько раз)Рефакторим тесты API с использованием этих методов. Где нет авторизации, остаются старые методы
perform
Проверяемся: gradle test
Код открытый, можно модифицировать. Ссылка на источник не обязательна, но, если сделаете, буду признателен:) Если код нравится - проголосуйте "за", если нет - напишите замечание в комментарии. Если вы ждали объяснений работы JWT - не надо минусовать статью - вы плохо прочли аннотацию к ней.
Приятного кодинга!