Доброе время!

Часть 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 object JwtUser (с @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 - не надо минусовать статью - вы плохо прочли аннотацию к ней.

Приятного кодинга!

Комментарии (0)