Команда Spring АйО перевела статью о работе с JWT в Spring Security с примерами и объяснениями. Прочитав данную статью, вы узнаете, как сделать первые шаги в создании приложения, в котором за безопасность отвечают токены JWT и фреймворк Spring Security, работающие в гармоничном тандеме.


Spring Security предоставляет полноценный набор возможностей для работы с безопасностью в Java приложениях, включая аутентификацию, авторизацию, управление сессиями и защиту от распространенных угроз безопасности, таких как  CSRF (Cross-Site Request Forgery). Фреймворк Spring Security хорошо приспособлен для кастомизации и позволяет разработчикам редактировать конфигурации безопасности в зависимости от нужд их приложения. Он предоставляет гибкую архитектуру и поддерживает различные механизмы аутентификации, такие как Basic Authentication, JWT и OAuth.

Spring Security предоставляет Basic Authentication из коробки. Чтобы понять, как это работает, вы можете обратиться к этой статье. Что касается данной статьи, здесь мы более пристально посмотрим на работу JWT и как сконфигурировать Spring Security для работы с этим механизмом.

Пример кода

Эта статья сопровождается рабочим примером кода на GitHub

Что такое JWT

JWT (JSON Web Token) — это безопасный способ передать сообщение в формате JSON между двумя сторонами. Этот стандарт задается в RFC 7519. Информация, содержащаяся в JWT токене, может быть проверена и ей можно доверять, потому что она подписана цифровой подписью. JWT токены можно подписывать, используя один симметричный ключ (например, созданный с помощью алгоритма HMAC) или пару ключей (публичный/приватный), созданных с помощью RSA или ECDSA.

Комментарий от редакции Spring АйО

Вернее было бы сказать, что JWT это на самом деле стандарт для создания токенов доступа в формате JSON. Когда говорят, что абстрактный токен "Х" является JWT токеном, имеется в виду, что токен "Х" создан согласно стандарту JWT.

ECDSA (алгоритм подписи с использованием эллиптических кривых) является более современным по сравнению с RSA, и рекомендуется использовать именно его.

К тому же, HMAC сам по себе не является алгоритмом подписи, автор здесь немного ошибается. HMAC можно охарактеризовать как способ создания MAC (он же Message Authentication Code, не путать с MAC адресом) на основе определенных hash функций, таких как, например, из семейства SHA.

В этой статье мы покажем, как создать JWT токен с использованием секретного симметричного ключа и затем воспользоваться им для того, чтобы обезопасить наши REST эндпоинты.

Структура JWT

В этом разделе мы посмотрим на пример структуры JWT токена. JSON веб токен состоит из трех частей:

  • Заголовок

  • Payload (она же “Рабочая нагрузка”), содержащий claim-ы

  • Подпись

Комментарий от редакции Spring АйО

Если придираться, то, строго говоря, согласно стандарту, цифровая подпись не является обязательной, но на практике без нее JWT не выпускают, т.к. без цифровой подписи невозможно проверить JWT токен на предмет сторонней модификации

JWT заголовок

Заголовок состоит из двух частей: типа токена, например, JWT, и использованного алгоритма подписи, например, SHA-256 или RSA. Пример JSON заголовка:

{
  "alg": "HS256",
  "typ": "JWT"
}

Этот JSON затем кодируется с использованием Base64, таким образом формируя первую часть JWT токена.

Рабочая нагрузка/Payload JWT

Рабочая нагрузка — это тело токена, содержащее данные как таковые. Это могут быть данные пользователя или любая информация, которую надо безопасно передать. Эти данные также называются claim-ами. Есть три типа claim-ов: зарегистрированные, публичные и приватные.

Зарегистрированные claim-ы 

Они являются набором предзаданных, состоящих из трех символов claim-ов, как определено в RFC7519. Некоторые часто используемые зарегистрированные claim-ы следующие iss (Issuer Claim), sub (Subject Claim), aud (Audience Claim), exp (Expiration Time Claim), iat (Issued At Time), nbf (Not Before). Давайте рассмотрим каждый из них поподробнее:

  • iss: Этот claim используется для обозначения издателя JWT токена. Он используется, чтобы идентифицировать сущность, которая выпустила токен, например, сервер аутентификации или провайдер идентификации.

  • sub: Этот claim используется для обозначения субъекта JWT токена, то есть пользователя или сущности, для которой токен был выпущен.

  • aud: Этот claim используется для обозначения целевой аудитории JWT токена. Обычно используется для ограничения использования токена, предназначенного только для определенных сервисов или приложения. 

  • exp: Этот claim используется, чтобы задать время истечения токена, после которого токен больше не считается валидным. Выражается в секундах с начала эпохи Unix.

  • iat: Время выпуска JWT токена. Может использоваться для определения возраста JWT. Выражается в секундах с начала эпохи Unix.

  • nbf: Задает время, до наступления которого JWT токен не может быть принят к обработке.

Чтобы посмотреть полный список зарегистрированных claim-ов, перейдите по ссылке. Далее в статье мы рассмотрим несколько примеров их использования.

Публичные claim-ы

В отличие от зарегистрированных claim-ов, которые зарезервированы, и их предназначение определено заранее, эти claim-ы могут кастомизироваться в зависимости от требований приложения. Большинство публичных claim-ов попадают в одну из перечисленных ниже категорий:

  • Данные пользователя/клиента: включают имя пользователя, clientId, электронный адрес, физический адрес, роли, скоупы, привилегии и любую информацию, относящуюся к пользователю/клиенту и используемую для аутентификации или авторизации.

  • Данные приложения: включают детали сессии, предпочтения пользователя (например, языковые), настройки приложения или другие данные, специфические для приложения.

  • Информация по безопасности: включает дополнительную информацию, относящуюся к безопасности, такую как ключи, сертификаты, токены и т.д.

Комментарий от редакции Spring АйО

На самом деле, разница между registered и public claim-ами весьма фиктивная. Он заключается  лишь в том, что registered клеймы прошли отдельную процедуру approval-а для того, чтобы попасть в непосредственно IANA стандарт. А public клеймы, несмотря на то, что также, как и registered клеймы являются публично заадопчеными, не проходили данной процедуры (по крайней мере пока что). То есть различие в основном в формальности соблюдения процедуры и стандартизации значений.

Приватные claim-ы

Приватные claim-ы — это кастомизированные claim-ы, специфические для определенной организации. Они не стандартизированы официальной JWT спецификацией, но определяются сторонами, вовлеченными в обмен JWT токенами в рамках организации.

Рекомендованные лучшие практики по JWT claim-ам 

  • Используйте стандартные claim-ы, заданные в JWT спецификации где только возможно. Они имеют широкое признание, и их предназначение хорошо определено. 

  • Рабочая нагрузка JWT токена должна содержать минимальное количество claim-ов для того, чтобы обеспечивать простоту поддержки и ограничить размер токена. 

  • Публичные claim-ы должны иметь понятные имена, хорошо описывающие их суть. 

  • Следуйте постоянным соглашениям по именованию, чтобы обеспечивать стабильность и читаемость.

  • Избегайте включения PII информации, чтобы минимизировать риск попадания данных в публичное пространство. 

  • Убедитесь в том, что JWT токены подписаны рекомендованными алгоритмами, определенными в зарегистрированном claim-е alg. Значение none в claim-е alg означает, что токен не подписан и не рекомендуется к использованию.

Комментарий от редакции Spring АйО

Здесь под PII имеется в виду Personally Identifiable Information. К ним можно отнести любые данные, которые позволяют идентифицировать человека. Например, номер телефона, номер в налоговой инспекции и т.д.

JWT подпись

Чтобы создать подпись, мы шифруем заголовок, шифруем рабочую нагрузку и используем ключ (симметричный или ассиметричный), чтобы подписать элементы алгоритмом, заданным в заголовке. Получившийся в результате токен будет содержать три строки в формате Base64, разделенные точками. Точечное представление JWT токена показано ниже:

Цель подписи состоит в том, чтобы проверить, что сообщение не было изменено при передаче. Поскольку заголовок и полезная нагрузка также подписываются секретным ключом, можно проверить, что отправитель именно тот, кем он себя называет.

Комментарий от редакции Spring АйО

Строго говоря, все, что говорит успешная цифровой подписи проверка, так это то, что подпись была поставлена сущностью, которая обладает либо:

- В случае симметричного шифрования, таким же симметричным ключом
- В случае ассиметричного шифрования, обладает вторым комплиментарным ключом.

Предполагается, что данным ключом может  обладать только trusted authority, то есть наш доверенный сервер, поэтому, такой подписи верят.

Распространенные сценарии использования JWT

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

  • Single Sign-On: JWT токены упрощают Single Sign-On (SSO единый вход), позволяя пользователю аутентифицироваться сразу в нескольких сервисах или приложениях. После того, как пользователь залогинился в одно приложение, он получает JWT токен, который можно использовать в других сервисах (к которым пользователь имеет доступ) без необходимости вводить/поддерживать отдельные креденшелы для логинов. 

  • API аутентификация: JWT токены часто используются для аутентификации и авторизации доступа к API. Клиенты включают JWT токен в заголовок Authorization API запроса, чтобы провалидировать доступ к API. Затем API декодируют JWT токены, чтобы предоставить доступ или отклонить запрос. 

  • Stateless сессии: JWT токены помогают управлять stateless сессиями, поскольку информация о сессии сохранена в самом токене. 

  • Обмен информацией: Поскольку JWT токены безопасны и надежны, их можно использовать для обмена не только информацией о пользователях, но и любой информацией, которую необходимо безопасно передать между двумя сторонами.

  • Микросервисы: JWT токены являются предпочтительным средством коммуникации между API в экосистеме микросервисов, поскольку микросервис может независимо проверить токен, не полагаясь на внешний сервер аутентификации, что упрощает масштабирование. 

Комментарий от редакции Spring АйО

Внимание! Имейте в виду, что утеря JWT token-а очень опасна, т.к. позволит стороннему пользователю выдать себя за того, для кого JWT токен был выдан. Поэтому "безопасный" не означает, что JWT токен можно передавать в свободном виде.

Проблемы с использованием JWT

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

  • В тех случаях, когда JWT токены заменяют сессии, если мы по итогу начнем использовать большие тела токенов (они же payload/рабочая нагрузка), JWT токен может раздуться. Более того, если мы добавим криптографическую подпись, это может вызвать общую перегрузку по производительности. Это приведет к избыточным затратам на хранение простой пользовательской сессии.

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

Комментарий от редакции Spring АйО

На деле, хранение сессии и работа "по-старинке" через SessionID, как правило, гораздо дороже с точки зрения производительности, чем проверка подписи и парсинг JWT.

Теперь, когда мы посмотрели на основные моменты, у нас появилась возможность принимать решение по поводу того, как и когда использовать JWT токены. В следующем разделе мы создадим простой JWT токен в Java.

Создание JWT токена в Java

JJWT — это наиболее часто используемая Java библиотека для создания JWT токенов в Java и Android. Мы можем начать с добавления ее зависимостей к нашему приложению.

Конфигурируем JWT зависимости 

Зависимости для Maven:

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.1</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.1</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.1</version>
    <scope>runtime</scope>
</dependency>

Зависимости для Gradle:

compile 'io.jsonwebtoken:jjwt-api:0.11.1'
runtime 'io.jsonwebtoken:jjwt-impl:0.11.1'
runtime 'io.jsonwebtoken:jjwt-jackson:0.11.1'

Наше Java приложение базируется на Maven, поэтому мы добавим приведенные выше зависимости для Maven к нашему pom.xml.

Создание JWT токена 

Мы будем использовать класс Jwts из пакета io.jsonwebtoken. Мы можем задать claim-ы (как зарегистрированные, так и публичные) и другие JWT атрибуты и создать токен, как показано ниже:

public static String createJwt() {
        return Jwts.builder()
                .claim("id", "abc123")
                .claim("role", "admin")
                /*.addClaims(Map.of("id", "abc123",
                        "role", "admin"))*/
                .setIssuer("TestApplication")
                .setIssuedAt(java.util.Date.from(Instant.now()))
                .setExpiration(Date.from(Instant.now().plus(10, ChronoUnit.MINUTES)))
                .compact();
    }

Этот метод создает следующий JWT токен:

eyJhbGciOiJub25lIn0.eyJpZCI6ImFiYzEyMyIsInJvbGUiOiJhZG1pbiIsImlzcyI6IlR
lc3RBcHBsaWNhdGlvbiIsImlhdCI6MTcxMTY2MTA1MiwiZXhwIjoxNzExNjYxNjUyfQ.

Теперь давайте посмотрим на методы сборщика, используемые для генерации токена:

  • claim: позволяет нам задавать любое количество кастомизированных claim-ов в виде пар ключ/значение. В качестве альтернативы, мы также можем использовать метод  addClaims, чтобы добавить Map-у claim-ов.

  • setIssuer: этот метод соответствует зарегистрированному claim-у iss.

  • setIssuedAt: этот метод соответствует зарегистрированному claim-у iat. Метод принимает java.util.Date в качестве параметра. Здесь мы установили значение текущего даты/времени. 

  • setExpiration: этот метод соответствует зарегистрированному claim-у exp. Метод принимает java.util.Date в качестве параметра. В примере мы указали, что токен истекает через 10 минут после его выдачи.

Давайте попробуем декодировать этот JWT токен, используя онлайн JWT декодер:

Если мы внимательно посмотрим на заголовок, мы увидим alg:none. Это из-за того, что мы не задали никакого алгоритма для использования. Как мы уже видели ранее, рекомендуется использовать алгоритм для генерации подписи. 

Поэтому, давайте воспользуемся в нашем методе алгоритмом HMAC SHA256:

public static String createJwt() {
        // Recommended to be stored in Secret
        String secret = "5JzoMbk6E5qIqHSuBTgeQCARtUsxAkBiHwdjXOSW8kWdXzYmP3X51C0";
        Key hmacKey = new SecretKeySpec(Base64.getDecoder().decode(secret),
                SignatureAlgorithm.HS256.getJcaName());
        return Jwts.builder()
                .claim("id", "abc123")
                .claim("role", "admin")
                .setIssuer("TestApplication")
                .setIssuedAt(java.util.Date.from(Instant.now()))
                .setExpiration(Date.from(Instant.now().plus(10, ChronoUnit.MINUTES)))
                .signWith(hmacKey)
                .compact();
    }

Получившийся в результате токен выглядит вот так:

eyJthbGciOiJIUzI1NiJ9.eyJpZCI6ImFiYzEyMyIsInJvbGUiOiJhZG1pbiIsImlz
cyI6IlRlc3RBcHBsaWNhdGlvbiIsImlhdCI6MTcxMjMyODQzMSwiZXhwIjoxNzEyMzI5MDMxfQ.
pj9AvbLtwITqBYazDnaTibCLecM-cQ5RAYw2YYtkyeA

Декодировав JWT, мы увидим:

Парсинг JWT токена

Теперь, когда мы создали JWT токен, давайте посмотрим на то, как парсить токены, чтобы извлекать из них claim-ы. Мы можем распарсить токен, только если мы знаем секретный ключ, который использовался при создании токена (либо его ассиметричную пару, в случае работы с RSA или ECDSA). Приведенный ниже код может использоваться для достижения этой цели:

public static Jws<Claims> parseJwt(String jwtString) {
        // Recommended to be stored in Secret
        String secret = "5JzoMbk6E5qIqHSuBTgeQCARtUsxAkBiHwdjXOSW8kWdXzYmP3X51C0";
        Key hmacKey = new SecretKeySpec(Base64.getDecoder().decode(secret),
                SignatureAlgorithm.HS256.getJcaName());

        Jws<Claims> jwt = Jwts.parserBuilder()
                .setSigningKey(hmacKey)
                .build()
                .parseClaimsJws(jwtString);

        return jwt;
    }

Здесь метод parseJwt принимает JWT токен как аргумент типа String. С учетом того, что мы использовали симметричное шифрование, используя тот же самый секретный ключ, который был использован при создании токена, этот токен можно распарсить, чтобы извлечь claim-ы. Это можно проверить с помощью приведенного ниже теста:

    @Test
    public void testParseJwtClaims() {
        String jwtToken = JWTCreator.createJwt();
        assertNotNull(jwtToken);
        Jws<Claims> claims = JWTCreator.parseJwt(jwtToken);
        assertNotNull(claims);
        Assertions.assertAll(
                () -> assertNotNull(claims.getSignature()),
                () -> assertNotNull(claims.getHeader()),
                () -> assertNotNull(claims.getBody()),
                () -> assertEquals(claims.getHeader().getAlgorithm(), "HS256"),
                () -> assertEquals(claims.getBody().get("id"), "abc123"),
                () -> assertEquals(claims.getBody().get("role"), "admin"),
                () -> assertEquals(claims.getBody().getIssuer(), "TestApplication")
        );
    }

Чтобы увидеть полный список доступных методов парсинга, обратитесь к документации.

Сравнение Basic Authentication и JWT в Spring Security

Прежде чем мы углубимся в реализацию JWT в примере Spring Boot приложения, давайте сравним BasicAuth и JWT по нескольким пунктам.

Сравнение по

Basic Authentication

JWT

Заголовкам авторизации

Пример Basic Auth заголовка: Authorization: Basic xxx.

Пример JWT заголовка: Authorization: Bearer xxx.

Валидности и истечению

Креденшелы Basic Authentication конфигурируются один раз, и те же самые креденшелы должны передаваться при каждом запросе. Они никогда не истекут.

С JWT токеном мы можем задавать валидность и время истечения, используя зарегистрированный claim exp, после чего токен выбрасывает исключение io.jsonwebtoken.ExpiredJwtException. Это делает JWT безопаснее, так как срок действия токена короткий. Пользователю придется заново отправить запрос, чтобы сгенерировать новый токен. 

Данным

Basic Authentication работает только с креденшелами (обычно имя пользователя и пароль).

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

Реализация JWT в Spring Boot приложении

Теперь, когда мы лучше понимаем JWT, давайте реализуем его в простом Spring Boot приложении. В нашем pom.xml добавим следующие зависимости:

        <dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt-api</artifactId>
			<version>0.11.1</version>
		</dependency>
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt-impl</artifactId>
			<version>0.11.1</version>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt-jackson</artifactId>
			<version>0.11.1</version>
			<scope>runtime</scope>
		</dependency>

Мы создали простое приложение “Library” (“Библиотека”) на Spring Boot, которое использует базу данных H2 для хранения данных в оперативной памяти. Приложение сконфигурировано для работы на порту 8083. Чтобы запустить приложение, выполним:

mvnw clean verify spring-boot:run (for Windows)
./mvnw clean verify spring-boot:run (for Linux)

Перехватывание цепочки фильтров Spring Security для JWT

У приложения имеется REST эндпоинт /library/books/all для получения всех книг, сохраненных в БД. Если мы выполним вот этот запрос через Postman, мы получим ошибку 401 UnAuthorized:

Это происходит потому, что зависимость spring-boot-starter-security, добавленная в наш pom.xml, автоматически включает Basic authentication фильтр на всех созданных эндпоинтах. Поскольку мы не задали никаких креденшелов в Postman, мы получаем ошибку Unauthorized. Для целей этой статьи нам надо заменить Basic Authentication на JWT аутентификацию. Мы знаем, что Spring предоставляет средства безопасности для наших эндпоинтов, вызывая цепочку фильтров, которые управляют аутентификацией и авторизацией для всех запросов. UsernamePasswordAuthenticationFilter отвечает за валидацию Basic креденшелов для каждого запроса. Чтобы отменить вызов этого фильтра, давайте создадим новый Filter, называемый JwtFilter. Этот фильтр будет расширять класс OncePerRequestFilter, поскольку мы хотим, чтобы этот фильтр вызывался только один раз для каждого запроса:

@Component
@Slf4j
public class JwtFilter extends OncePerRequestFilter {

    private final AuthUserDetailsService userDetailsService;

    private final JwtHelper jwtHelper;

    public JwtFilter(AuthUserDetailsService userDetailsService, JwtHelper jwtHelper) {
        this.userDetailsService = userDetailsService;
        this.jwtHelper = jwtHelper;
    }


    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                    HttpServletResponse response, 
                                    FilterChain filterChain) 
            throws ServletException, IOException {
        log.info("Inside JWT filter");
        // Code to validate the Authorization header
    }
}

Класс JwtHelper отвечает за создание и валидацию токена. Сначала давайте посмотрим, как создать токен:

public String createToken(Map<String, Object> claims, String subject) {
    Date expiryDate = 
        Date.from(Instant.ofEpochMilli(System.currentTimeMillis() + 
        jwtProperties.getValidity()));
    Key hmacKey = new SecretKeySpec(Base64.getDecoder()
        .decode(jwtProperties.getSecretKey()),
            SignatureAlgorithm.HS256.getJcaName());
    return Jwts.builder()
            .setClaims(claims)
            .setSubject(subject)
            .setIssuedAt(new Date(System.currentTimeMillis()))
            .setExpiration(expiryDate)
            .signWith(hmacKey)
            .compact();
}

Следующие параметры отвечают за создание токена:

  • claims ссылается на пустую Map. В этом примере не были определены никакие специфические claim-ы.

  • subject обозначает имя пользователя, переданное пользователем при вызове API для создания токена.

  • expiryDate обозначает дату, которая получается при добавлении ‘x’ миллисекунд к текущему таймстемпу. Значение ‘x’ задается через проперти jwt.validity.

  • hmacKey относится к объекту java.security.Key, использованному для подписания JWT запроса. Для этого примера использовался секрет, заданный в проперти jwt.secretKey, при этом использовался алгоритм HS256.

Комментарий от редакции Spring АйО

HS256 это просто, HMAC  с использованием SHA-256

Этот метод возвращает токен в формате String, который надо передавать в  Authorization заголовок с каждым запросом. Теперь, когда мы создали токен, давайте посмотрим на метод doFilterInternal в классе JwtFilter и поймем, за что отвечает этот класс Filter:

@Override
protected void doFilterInternal(
    HttpServletRequest request, 
    HttpServletResponse response, 
    FilterChain filterChain
) throws ServletException, IOException {
    
      final String authorizationHeader = request.getHeader(AUTHORIZATION);
      String jwt = null;
      String username = null;
      if (Objects.nonNull(authorizationHeader) && 
              authorizationHeader.startsWith("Bearer ")) {
          jwt = authorizationHeader.substring(7); // length of “Bearer “
          username = jwtHelper.extractUsername(jwt);
      }

      if (Objects.nonNull(username) && 
              SecurityContextHolder.getContext().getAuthentication() == null) {
          UserDetails userDetails = 
              this.userDetailsService.loadUserByUsername(username);
          boolean isTokenValidated = 
              jwtHelper.validateToken(jwt, userDetails);
          if (isTokenValidated) {
              UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
                      new UsernamePasswordAuthenticationToken(
                                  userDetails, null, userDetails.getAuthorities());
              usernamePasswordAuthenticationToken.setDetails(
                      new WebAuthenticationDetailsSource().buildDetails(request));
              SecurityContextHolder.getContext().setAuthentication(
                      usernamePasswordAuthenticationToken);
          }
      }
  
  filterChain.doFilter(request, response);
}

Шаг 1. Считывает заголовок Authorization и извлекает строку JWT.

Шаг 2. Парсит строку JWT и извлекает имя пользователя. Для этой цели мы используем библиотеку io.jsonwebtoken Jwts.parseBuilder(). jwtHelper.extractUsername() выглядит как показано ниже:

public String extractUsername(String bearerToken) {
        return extractClaimBody(bearerToken, Claims::getSubject);
    }
public <T> T extractClaimBody(String bearerToken, 
            Function<Claims, T> claimsResolver) {
        Jws<Claims> jwsClaims = extractClaims(bearerToken);
        return claimsResolver.apply(jwsClaims.getBody());
        }
private Jws<Claims> extractClaims(String bearerToken) {
        return Jwts.parserBuilder().setSigningKey(jwtProperties.getSecretKey())
        .build().parseClaimsJws(bearerToken);
        }

Шаг 3. Как только мы извлекли имя пользователя, мы проверяем, доступен ли валидный Authentication объект, например, залогиненный пользователь, используя SecurityContextHolder.getContext().getAuthentication(). Если такого объекта нет, мы используем UserDetailsService от Spring Security, чтобы загрузить объект UserDetails. Для этого примера мы создали класс AuthUserDetailsService, который возвращает объект UserDetails.

public class AuthUserDetailsService implements UserDetailsService {

    private final UserProperties userProperties;

    @Autowired
    public AuthUserDetailsService(UserProperties userProperties) {
        this.userProperties = userProperties;
    }


    @Override
    public UserDetails loadUserByUsername(String username) 
            throws UsernameNotFoundException {

        if (StringUtils.isEmpty(username) || 
                !username.equals(userProperties.getName())) {
            throw new UsernameNotFoundException(
                    String.format("User not found, or unauthorized %s", username));
        }

        return new User(userProperties.getName(), 
                userProperties.getPassword(), new ArrayList<>());
    }
}

Имя пользователя и пароль, заключенные в UserProperties, загружаются из application.yml как:

spring:
  security:
    user:
      name: libUser
      password: libPassword

Шаг 4. Далее, фильтр JwtFilter вызывает jwtHelper.validateToken(), чтобы валидировать извлеченное имя пользователя и убеждается в том, что JWT токен не истек.

public boolean validateToken(String token, UserDetails userDetails) {
        final String userName = extractUsername(token);
        return userName.equals(userDetails.getUsername()) && !isTokenExpired(token);
    }
private Boolean isTokenExpired(String bearerToken) {
        return extractExpiry(bearerToken).before(new Date());
        }
public Date extractExpiry(String bearerToken) {
        return extractClaimBody(bearerToken, Claims::getExpiration);
        }

Шаг 5. Как только мы валидировали токен, мы создаем экземпляр объекта Authentication. Здесь создается объект UsernamePasswordAuthenticationToken (который является реализацией интерфейса Authentication), и затем он устанавливается в SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken). Это означает, что пользователь теперь аутентифицирован. 

Шаг 6. И, наконец, мы вызываем filterChain.doFilter(request, response), чтобы FilterChain вызвала следующий фильтр.

Таким образом мы успешно создали класс фильтра для валидации токена. Мы рассмотрим обработку исключений в следующих разделах. 

Эндпоинты для создания JWT токенов 

В этом разделе мы создадим класс Controller, чтобы создать эндпоинт, который позволит нам создавать токены в виде строки. Этот токен будет задан в заголовке Authorization, когда мы будем вызывать наше приложение. Давайте создадим класс TokenController:

@RestController
public class TokenController {

    private final TokenService tokenService;

    public TokenController(TokenService tokenService) {
        this.tokenService = tokenService;
    }

    @PostMapping("/token/create")
    public TokenResponse createToken(@RequestBody TokenRequest tokenRequest) {
        return tokenService.generateToken(tokenRequest);
    }
}

Класс тела запроса TokenRequest будет принимать имя пользователя и пароль:

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class TokenRequest {
    private String username;
    private String password;
}

Класс TokenService отвечает за валидацию креденшелов, переданных в теле запроса и за вызов jwtHelper.createToken(), как описано в предыдущем разделе. Чтобы аутентифицировать креденшелы, нам надо реализовать AuthenticationManager. Давайте создадим класс SecurityConfiguration, чтобы задать все конфигурации, относящиеся к Spring Security.

@Configuration
@EnableWebSecurity
public class SecurityConfiguration {

    private final JwtFilter jwtFilter;

    private final AuthUserDetailsService authUserDetailsService;

    private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;

    @Autowired
    public SecurityConfiguration(JwtFilter jwtFilter,
                                 AuthUserDetailsService authUserDetailsService,
                                 JwtAuthenticationEntryPoint 
                                             jwtAuthenticationEntryPoint) {

        this.jwtFilter = jwtFilter;
        this.authUserDetailsService = authUserDetailsService;
        this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;
    }

    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        final DaoAuthenticationProvider daoAuthenticationProvider = 
                new DaoAuthenticationProvider();
        daoAuthenticationProvider.setUserDetailsService(authUserDetailsService);
        daoAuthenticationProvider.setPasswordEncoder(
                PlainTextPasswordEncoder.getInstance());
        return daoAuthenticationProvider;
    }

    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity httpSecurity) 
            throws Exception {
        return httpSecurity.getSharedObject(AuthenticationManagerBuilder.class)
                .authenticationProvider(authenticationProvider())
                .build();
    }
}

AuthenticationManager использует AuthUserDetailsService, который в свою очередь использует свойство spring.security.user. Теперь, когда у нас есть AuthenticationManager, давайте посмотрим на то, как задается TokenService:

Комментарий от редакции Spring АйО

Так, как показывает автор, сделать, безусловно, можно. Однако AuthencitaionManager изначально задумывался для использования через ProviderManager API. Иными словами, напрямую AuthencitaionManager, как правило, не используют, только в исключительных случаях.

@Service
public class TokenService {

    private final AuthenticationManager authenticationManager;

    private final AuthUserDetailsService userDetailsService;

    private final JwtHelper jwtHelper;

    public TokenService(AuthenticationManager authenticationManager,
                        AuthUserDetailsService userDetailsService,
                        JwtHelper jwtHelper) {
        this.authenticationManager = authenticationManager;
        this.userDetailsService = userDetailsService;
        this.jwtHelper = jwtHelper;
    }


    public TokenResponse generateToken(TokenRequest tokenRequest) {
        this.authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                        tokenRequest.getUsername(), tokenRequest.getPassword()));
        final UserDetails userDetails = 
                userDetailsService.loadUserByUsername(tokenRequest.getUsername());
        String token = jwtHelper.createToken(
                Collections.emptyMap(), userDetails.getUsername());
        return TokenResponse.builder()
                .token(token)
                .build();
    }
}

TokenResponse — это Response объект, который содержит строку токена:

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class TokenResponse {

    private String token;

}

Теперь, когда мы создали API, давайте запустим наше приложение и попробуем обратиться к эндпоинту через Postman. Мы видим ошибку 401 Unauthorized как показано ниже:

Причина этого та же самая, что и ранее. Spring Security закрывает все эндпоинты по умолчанию. Нам нужен способ исключить эндпоинт для создания токенов из числа закрываемых. Более того, в логах запуска мы видим, что, хотя мы задали JwtFilter и ожидаем, что он будет использован вместо UsernamePasswordAuthenticationFilter, мы не видим, чтобы этот фильтр был добавлен в цепочку фильтров безопасности, как показано ниже:

2024-05-22 15:41:09.441  INFO 20432 --- [           main] 
o.s.s.web.DefaultSecurityFilterChain     : 
Will secure any request with 
    [org.springframework.security.web.session.DisableEncodeUrlFilter@14d36bb2, 
org.springframework.security.web.context.request.async.
    WebAsyncManagerIntegrationFilter@432448, 
org.springframework.security.web.context.SecurityContextPersistenceFilter@54d46c8, 
org.springframework.security.web.header.HeaderWriterFilter@c7cf8c4, 
org.springframework.security.web.csrf.CsrfFilter@17fb5184, 
org.springframework.security.web.authentication.logout.LogoutFilter@42fa5cb, 
org.springframework.security.web.authentication.
    UsernamePasswordAuthenticationFilter@70d7a49b, 
org.springframework.security.web.authentication.ui.
    DefaultLoginPageGeneratingFilter@67cd84f9, 
org.springframework.security.web.authentication.ui.
    DefaultLogoutPageGeneratingFilter@4452e13c, 
org.springframework.security.web.authentication.www.
    BasicAuthenticationFilter@788d9139, 
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@5c34b0f2, 
org.springframework.security.web.servletapi.
    SecurityContextHolderAwareRequestFilter@7dfec0bc, 
org.springframework.security.web.authentication.
    AnonymousAuthenticationFilter@4d964c9e, 
org.springframework.security.web.session.SessionManagementFilter@731fae, 
org.springframework.security.web.access.ExceptionTranslationFilter@66d61298, 
org.springframework.security.web.access.intercept.FilterSecurityInterceptor@55c20a91]

Чтобы добавить JwtFilter в цепочку к остальным фильтрам и исключить эндпоинт для создания токенов из числа защищаемых JwtFilter-ом, давайте создадим бин SecurityFilterChain в нашем классе SecurityConfiguration:

    @Bean
    public SecurityFilterChain configure (HttpSecurity http) throws Exception {
        return http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/token/*").permitAll()
                .anyRequest().authenticated().and()
                .sessionManagement(session -> 
                    session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .addFilterBefore(jwtFilter, 
                    UsernamePasswordAuthenticationFilter.class)
                .exceptionHandling(exception -> 
                    exception.authenticationEntryPoint(jwtAuthenticationEntryPoint))
                .build();
    }

В этой конфигурации нас интересует следующее:

  • antMatchers("/token/*").permitAll() — это позволит исключить API эндпоинты, которые соответствуют паттерну /token/*, из числа защищенных.

  • anyRequest().authenticated() — Spring Security защитит все остальные API запросы.

  • addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) — это включит JwtFilter в цепочку фильтров до UsernamePasswordAuthenticationFilter.

  • exceptionHandling(exception -> exception.authenticationEntryPoint(jwtAuthenticationEntryPoint) — в случае появления исключения аутентификации будет вызван класс JwtAuthenticationEntryPoint. Здесь мы создали класс JwtAuthenticationEntryPoint, который реализует org.springframework.security.web.AuthenticationEntryPoint, чтобы корректно обработать ошибку отсутствия авторизации. Мы посмотрим на обработку ошибок более детально в следующих разделах.

После всех этих изменений, давайте перезапустим наше приложение и проверим логи:

2024-05-22 16:13:07.803  INFO 16188 --- [           main] 
o.s.s.web.DefaultSecurityFilterChain     : Will secure any request with 
[org.springframework.security.web.session.DisableEncodeUrlFilter@73e25780, 
org.springframework.security.web.context.request.async.
    WebAsyncManagerIntegrationFilter@1f4cb17b, 
org.springframework.security.web.context.SecurityContextPersistenceFilter@b548f51, 
org.springframework.security.web.header.HeaderWriterFilter@4f9980e1, 
org.springframework.security.web.authentication.logout.LogoutFilter@6b92a0d1, 
com.reflectoring.security.filter.JwtFilter@5961e92d, 
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@56976b8b, 
org.springframework.security.web.servletapi.
    SecurityContextHolderAwareRequestFilter@74844216, 
org.springframework.security.web.authentication.
    AnonymousAuthenticationFilter@280099a0, 
org.springframework.security.web.session.SessionManagementFilter@144dc2f7, 
org.springframework.security.web.access.ExceptionTranslationFilter@7a0f43dc, 
org.springframework.security.web.access.intercept.
    FilterSecurityInterceptor@735167e1]

Мы видим включение JwtFilter в цепочку, что означает, что Basic auth теперь будет заменена на аутентификацию через токен. Давайте снова попробуем зайти на эндпоинт /token/create. Теперь мы видим, что эндпоинт успешно возвращает сгенерированный токен:

Защищаем эндпоинты приложения

Теперь, когда мы можем успешно генерировать токены, нам надо передать этот токен в наше приложение, чтобы успешно вызвать /library/books/all. Давайте добавим заголовок Authorization типа Bearer Token со сгенерированным токеном и отправим запрос. Теперь мы видим ответ 200 OK, как показано ниже:

Обработка исключений при работе с JWT

В этом разделе мы посмотрим на некоторые часто встречающиеся исключения из пакета io.jsonwebtoken:

  1. ExpiredJwtException — JWT содержит время истечения, которое уже прошло. Когда токен распарсили, время истечения уже осталось в прошлом, и было выброшено исключение ExpiredJwtException.

  2. UnsupportedJwtException — Это исключение выбрасывается, когда получен JWT токен в формате, который не ожидался получателем. Чаще всего эта ошибка возникает, когда мы пытаемся парсить подписанный JWT токен методом Jwts.parserBuilder().setSigningKey(jwtProperties.getSecretKey()) .build().parseClaimsJwt вместо Jwts.parserBuilder().setSigningKey(jwtProperties.getSecretKey()) .build().parseClaimsJws

  3. MalformedJwtException — Это исключение указывает на то, что JWT токен неправильно составлен (составлен вне соответствия стандарту).

  4. IncorrectClaimException — Указывает на то, что обязательный claim не имеет ожидаемого значения.

MissingClaimException — Это исключение указывает на то, что обязательный claim отсутствует.

В целом, считается хорошей практикой корректно обрабатывать исключения, относящиеся к аутентификации. В случае базовой аутентификации, Spring Security по умолчанию добавляет BasicAuthenticationEntryPoint к цепочке фильтров безопасности, которая оборачивает ошибки, относящиеся к Basic Auth, в 401 Unauthorized. Похожим образом, в нашем примере мы явно создали JwtAuthenticationEntryPoint для обработки возможных ошибок аутентификации, таких как BadCredentialsException от Spring Security или MalformedJwtException от JJwt:

@Component
@Slf4j
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, 
                         HttpServletResponse response, 
                         AuthenticationException authException) 
            throws IOException, ServletException {
        Exception exception = (Exception) request.getAttribute("exception");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setContentType(APPLICATION_JSON_VALUE);
        log.error("Authentication Exception: {} ", exception, exception);
        Map<String, Object> data = new HashMap<>();
        data.put("message", exception != null ? 
                exception.getMessage() : authException.getCause().toString());
        OutputStream out = response.getOutputStream();
        ObjectMapper mapper = new ObjectMapper();
        mapper.writeValue(out, data);
        out.flush();
    }
}

В нашем классе JwtFilter мы добавляем сообщение об исключении к атрибуту HttpServletRequest. Это позволяет нам использовать request.getAttribute("exception") и вписывать его в ServletResponse

public class JwtFilter extends OncePerRequestFilter {

   @Override
   protected void doFilterInternal(HttpServletRequest request, 
                                   HttpServletResponse response, 
                                   FilterChain filterChain) 
           throws ServletException, IOException {
      try {
         //validate token here
      } catch (ExpiredJwtException jwtException) {
         request.setAttribute("exception", jwtException);
      } catch (BadCredentialsException | 
               UnsupportedJwtException | 
               MalformedJwtException e) {
         log.error("Filter exception: {}", e.getMessage());
         request.setAttribute("exception", e);
      }
      filterChain.doFilter(request, response);
   }
}

После внесения этих изменений мы теперь видим сообщение для всех исключений вида 401 Unauthorized, как показано ниже:

Однако, важно отметить, что JwtFilter вызывается только для эндпоинтов, которые защищены Spring Security через ее цепочку фильтров. В нашем случае это эндпоинт /library/books/all. Поскольку мы исключили эндпоинт генерации токенов /token/create из числа защищенных Spring Security, обработка исключений, сделанная в JwtAuthenticationEntryPoint, к нему не применится. В таких случаях мы будем обрабатывать исключения с использованием глобального обработчика исключений от Spring.

@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler({BadCredentialsException.class})
    public ResponseEntity<Object> handleBadCredentialsException(BadCredentialsException exception) {
        return ResponseEntity
                .status(HttpStatus.UNAUTHORIZED)
                .body(exception.getMessage());
    }
}

При использовании такого способа обработки исключения, вызванные плохими креденшелами, будут обработаны через ошибку 401 Unauthorized:

Документация для Swagger 

В этом разделе мы посмотрим на то, как конфигурировать Open API для JWT. Мы добавим приведенную ниже зависимость для Maven:

<dependency>
   <groupId>org.springdoc</groupId>
   <artifactId>springdoc-openapi-ui</artifactId>
   <version>1.7.0</version>
</dependency>

Затем давайте добавим вот такую конфигурацию:

@OpenAPIDefinition(
        info = @Info(
                title = "Library application",
                description = "Get all library books",
                version = "1.0.0",
                license = @License(
                        name = "Apache 2.0",
                        url = "http://www.apache.org/licenses/LICENSE-2.0"
                )),
        security = {
                @SecurityRequirement(
                        name = "bearerAuth"
                )
        }
        )
@SecurityScheme(
        name = "bearerAuth",
        description = "JWT Authorization",
        scheme = "bearer",
        type = SecuritySchemeType.HTTP,
        bearerFormat = "JWT",
        in = SecuritySchemeIn.HEADER
)
public class OpenApiConfig {
}

Здесь безопасность описывается с использованием одной или нескольких аннотаций @SecurityScheme. Заданный здесь type, а именно SecuritySchemeType.HTTP, относится как к Basic Auth, так и к JWT. Другие атрибуты, такие как  scheme и bearerFormat, зависят от этого атрибута type. После задания схем безопасности мы можем применить их ко всему приложению или индивидуальным операциям, добавив раздел security на уровне root или на уровне операций. В нашем примере все API операции будут использовать схему аутентификации bearer token. Чтобы узнать больше о том, как конфигурировать несколько схем безопасности и применять разные схемы на уровне API level, см. документацию.

Теперь давайте добавим несколько базовых аннотаций от Swagger к нашим классам контроллеров, чтобы добавить описания API операций.

@RestController
@Tag(name = "Library Controller", description = "Get library books")
public class BookController {
}

@RestController
@Tag(name = "Create Token", description = "Create Token")
public class TokenController {
}

Кроме того, мы будем использовать приведенное ниже свойство, чтобы перезаписать URL, по которому загружается Swagger-UI от Springdoc.

springdoc:
  swagger-ui:
    path: /swagger-ui

С такой конфигурацией Swagger UI будет доступен по адресу http://localhost:8083/swagger-ui/index.html.

Давайте запустим приложение и попробуем загрузить страницу Swagger по упомянутому URL. Когда мы пробуем обратиться к эндпоинту, мы видим вот это: 

Это происходит потому, что все эндпоинты в приложении защищаются автоматически. Нам нужен способ явно исключить эндпоинт Swagger из числа защищаемых. Мы можем сделать это, добавив бин WebSecurityCustomizer и исключив эндпоинты Swagger в нашем классе in our SecurityConfiguration.

@Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return web -> web.ignoring().antMatchers(
                ArrayUtils.addAll(buildExemptedRoutes()));
    }

    private String[] buildExemptedRoutes() {
        return new String[] {"/swagger-ui/**","/v3/api-docs/**"};
    }

Теперь, когда мы запустим приложение, страница Swagger загрузится следующим образом: 

Поскольку у нас есть только одна схема безопасности, давайте добавим JWT токен к кнопке Authorize в верхней части страницы Swagger. 

Имея установленный bearer token, давайте попробуем зайти на эндпоинт /library/books/all:

Таким образом, мы успешно сконфигурировали в нашем приложении эндпоинты для Swagger.

Добавим тесты Spring Security

В нашем примере нам необходимы тесты, чтобы протестировать эндпоинт генерации токенов, и еще один тест для нашего приложения.

Давайте добавим несколько необходимых свойств для наших тестов, а также in-memory базу данных для работы с реальными данными. Протестируем application.yml:

spring:
  security:
    user:
      name: libUser
      password: libPassword
  datasource:
    driver-class-name: org.hsqldb.jdbc.JDBCDriver
    url: jdbc:hsqldb:mem:testdb;DB_CLOSE_DELAY=-1
    username: sa
    password:

jwt:
  secretKey: 5JzoMbk6E5qIqHSuBTgeQCARtUsxAkBiHwdjXOSW8kWdXzYmP3X51C0
  validity: 600000

Теперь напишем тесты для проверки эндпоинта токена:

@SpringBootTest
@AutoConfigureMockMvc
public class TokenControllerTest {
    @Autowired
    private MockMvc mvc;

    @Test
    public void shouldNotAllowAccessToUnauthenticatedUsers() throws Exception {
        TokenRequest request = TokenRequest.builder()
                .username("testUser")
                .password("testPassword")
                .build();
        mvc.perform(MockMvcRequestBuilders.post("/token/create")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(new ObjectMapper().writeValueAsString(request)))
                .andExpect(status().isUnauthorized());
    }

    @Test
    public void shouldGenerateAuthToken() throws Exception {
        TokenRequest request = TokenRequest.builder()
                .username("libUser")
                .password("libPassword")
                .build();
        mvc.perform(MockMvcRequestBuilders.post("/token/create")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(new ObjectMapper().writeValueAsString(request)))
                .andExpect(status().isOk());
    }
}

Здесь мы будем использовать @MockMvc для проверки эндпоинта нашего класса TokenController, эндпоинт должен работать как в позитивных, так и в негативных сценариях. 

Аналогичным образом, наш BookControllerTest будет выглядеть вот так:

@SpringBootTest
@AutoConfigureMockMvc
@SqlGroup({
        @Sql(value = "classpath:init/first.sql", 
                executionPhase = BEFORE_TEST_METHOD),
        @Sql(value = "classpath:init/second.sql", 
                executionPhase = BEFORE_TEST_METHOD)
})

public class BookControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void failsAsBearerTokenNotSet() throws Exception {
        mockMvc.perform(get("/library/books/all"))
                .andDo(print())
                .andExpect(status().isUnauthorized());
    }

    @Test
    void testWithValidBearerToken() throws Exception {
        TokenRequest request = TokenRequest.builder()
                .username("libUser")
                .password("libPassword")
                .build();
        MvcResult mvcResult = mockMvc.perform(
                MockMvcRequestBuilders.post("/token/create")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(new ObjectMapper().writeValueAsString(request)))
                .andExpect(status().isOk()).andReturn();
        String resultStr = mvcResult.getResponse().getContentAsString();
        TokenResponse token = new ObjectMapper().readValue(
                resultStr, TokenResponse.class);
        mockMvc.perform(get("/library/books/all")
                        .header("Authorization", "Bearer " + token.getToken()))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(jsonPath("$", hasSize(5)));
    }

    @Test
    void testWithInvalidBearerToken() throws Exception {
        mockMvc.perform(get("/library/books/all")
                        .header("Authorization", "Bearer 123"))
                .andDo(print())
                .andExpect(status().isUnauthorized());
    }

}

Чтобы протестировать эндпоинты приложения, мы будем использовать класс MockMvc от Spring и инициализировать нашу in-memory БД данными, используя SQL скрипты. Для этого воспользуемся аннотациями @SqlGroup и @Sql, а скрипты типа INSERT будут помещены внутри каталогов /resources/init.

Чтобы проверить успешный прогон эндпоинта testWithValidBearerToken(), мы сначала вызовем эндпоинт /token/create, используя MockMvc, извлечем токен из ответа и установим токен в заголовке Authorization для следующего вызова /library/books/all.

Заключение

По итогу можно сказать, что JWT аутентификация на шаг опережает Spring’s Basic Authentication от Spring, если говорить о безопасности. Это одна из наиболее популярных опций для аутентификации и авторизации. В данной статье мы рассмотрели некоторые лучшие практики, преимущества использования JWT, а также посмотрели на то, как сконфигурировать простое Spring Boot приложение, используя JWT для безопасности.


Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.

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