Усовершенствовать аутентификацию Java с помощью веб-токенов JSON (JWT)

Дополнительная аутентификация Java с помощью веб-токенов JSON (JWT)

Готовитесь к созданию безопасной аутентификации в приложении Java или боретесь с ней? Не уверены в преимуществах использования токенов (и, в частности, веб-токенов JSON), или как их следует развертывать? Я с радостью отвечу на эти и другие вопросы в этом уроке!

Прежде чем мы углубимся в веб-токены JSON (JWTs) иJJWT library (созданные техническим директором Stormpath Лесом Хазлвудом и поддерживаемыеcommunity of contributors), давайте рассмотрим некоторые основы.

1. Аутентификация против Токен аутентификации

Набор протоколов, который приложение использует для подтверждения личности пользователя, является аутентификацией. Приложения традиционно сохраняют идентичность посредством файлов cookie сеанса. Эта парадигма опирается на серверное хранилище идентификаторов сеансов, что вынуждает разработчиков создавать хранилище сеансов, которое либо уникально и специфично для сервера, либо реализовано как совершенно отдельный уровень хранилища сеансов.

Аутентификация с помощью токена была разработана для решения проблем, связанных с идентификаторами сеансов на стороне сервера, которых не было и не могло быть. Как и при традиционной аутентификации, пользователи предоставляют верифицируемые учетные данные, но теперь им выдается набор токенов вместо идентификатора сеанса. Начальными учетными данными могут быть стандартная пара имя пользователя / пароль, ключи API или даже токены из другой службы. (Примером аутентификации API-ключа Stormpath является пример этого.)

1.1. Почему токены?

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

Токены предлагают широкий спектр приложений, в том числе: схемы защиты от подделки межсайтовых запросов (CSRF), взаимодействияOAuth 2.0, идентификаторы сеансов и (в файлах cookie) в качестве представлений аутентификации. В большинстве случаев стандарты не определяют конкретный формат токенов. Вот пример типичногоSpring Security CSRF token в HTML-форме:

Если вы попытаетесь опубликовать эту форму без правильного токена CSRF, вы получите ответ об ошибке, и в этом польза токенов. Приведенный выше пример является «тупым» токеном. Это означает, что нет никакого внутреннего значения, которое можно было бы извлечь из самого токена. Это также, где JWT имеют большое значение.

Дальнейшее чтение:

Использование JWT с Spring Security OAuth

Руководство по использованию веб-токенов JSON с симметричной и асимметричной подписью в Spring Security OAuth.

Read more

Spring REST API + OAuth2 + Angular

Узнайте, как настроить OAuth2 для Spring REST API и как использовать его с клиентом Angular.

Read more

OAuth2 для API Spring REST - обработка маркера обновления в AngularJS

Мы узнали, как хранить токен обновления в клиентском приложении AngularJS, как обновить токен доступа с истекшим сроком действия и как использовать прокси Zuul.

Read more

2. Что в JWT?

JWT (произносится как «jots») - это URL-безопасные, закодированные, криптографически подписанные (иногда зашифрованные) строки, которые можно использовать в качестве токенов в различных приложениях. Вот пример использования JWT в качестве токена CSRF:

В этом случае вы можете видеть, что токен намного длиннее, чем в нашем предыдущем примере. Как мы уже видели, если форма отправлена ​​без токена, вы получите сообщение об ошибке.

Итак, почему JWT?

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

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

заголовок

eyJhbGciOiJIUzI1NiJ9

полезная нагрузка

eyJqdGkiOiJlNjc4ZjIzMzQ3ZTM0MTBkYjdlNjg3Njc4MjNiMmQ3MCIsImlhdC I6MTQ2NjYzMzMxNywibmJmIjoxNDYY2NjMzMzE3LC9JleNjMzMzE3LC9

Подпись

rgx_o8VQGuDa2AqCHSgVOD5G68Ld_YYM7N7THmvLIKc

Каждый раздел закодирован в URLbase64. Это гарантирует, что его можно безопасно использовать в URL (подробнее об этом позже). Рассмотрим подробнее каждый раздел по отдельности.

2.1. Заголовок

Если вы используете base64 для декодирования заголовка, вы получите следующую строку JSON:

{"alg":"HS256"}

Это показывает, что JWT был подписанHMAC с использованиемSHA-256.

2.2. Полезная нагрузка

Если вы декодируете полезную нагрузку, вы получите следующую строку JSON (отформатированную для ясности):

{
  "jti": "e678f23347e3410db7e68767823b2d70",
  "iat": 1466633317,
  "nbf": 1466633317,
  "exp": 1466636917
}

Как видите, в полезной нагрузке есть несколько ключей со значениями. Эти ключи называются «заявками», а вJWT specification семь из них указаны как «зарегистрированные» заявки. Они есть:

iss

эмитент

sub

Предмет

aud

Аудитория

exp

истечение

nbf

Не раньше, чем

iat

Выдано в

jti

JWT ID

При создании JWT вы можете добавить любые пользовательские требования, которые пожелаете. Приведенный выше список просто представляет утверждения, которые зарезервированы как в используемом ключе, так и в ожидаемом типе. Наша CSRF имеет идентификатор JWT, время «Выдано в», время «Не раньше» и время истечения срока действия. Время истечения составляет ровно одну минуту после выданного времени.

2.3. Подпись

Наконец, раздел подписи создается путем объединения заголовка и полезной нагрузки (вместе с. между ними) и прохождение через указанный алгоритм (в данном случае HMAC с использованием SHA-256) вместе с известным секретом. Обратите внимание, что секрет - этоalways байтовый массив, и его длина должна соответствовать используемому алгоритму. Ниже я использую случайную строку в кодировке base64 (для удобства чтения), которая преобразуется в массив байтов.

Это выглядит так в псевдокоде:

computeHMACSHA256(
    header + "." + payload,
    base64DecodeToByteArray("4pE8z3PBoHjnV1AhvGk+e8h2p+ShZpOnpr8cwHmMh1w=")
)

Пока вы знаете секрет, вы можете самостоятельно сгенерировать подпись и сравнить свой результат с разделом подписи JWT, чтобы убедиться, что он не был подделан. Технически JWT, подписанный криптографически, называетсяJWS. JWT также могут быть зашифрованы и тогда будут называтьсяJWE. (На практике термин JWT используется для описания JEW и JES.)

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

Что ж, мы здесь много сделали. Давайте погрузимся в код!

3. Настройте учебник JJWT

JJWT (https://github.com/jwtk/jjwt) - это библиотека Java, обеспечивающая сквозное создание и проверку веб-токенов JSON. Он всегда был бесплатным и с открытым исходным кодом (Apache License, версия 2.0). Он был разработан с интерфейсом, ориентированным на разработчиков, скрывающим большую часть его сложности.

Основные операции при использовании JJWT включают сборку и анализ JWT. Далее мы рассмотрим эти операции, затем рассмотрим некоторые расширенные функции JJWT, и, наконец, мы увидим JWT в действии как токены CSRF в приложении Spring Security, Spring Boot.

Код, продемонстрированный в следующих разделах, можно найтиhere. Примечание. В проекте с самого начала используется Spring Boot, так как он легко взаимодействует с предоставляемым API.

Чтобы построить проект, выполните следующее:

git clone https://github.com/eugenp/tutorials.git
cd tutorials/jjwt
mvn clean install

Одна из замечательных особенностей Spring Boot заключается в том, насколько легко запустить приложение. Чтобы запустить приложение JJWT Fun, просто сделайте следующее:

java -jar target/*.jar

В этом примере приложения представлено десять конечных точек (я использую httpie для взаимодействия с приложением. Его можно найтиhere.)

http localhost:8080
Available commands (assumes httpie - https://github.com/jkbrzt/httpie):

  http http://localhost:8080/
    This usage message

  http http://localhost:8080/static-builder
    build JWT from hardcoded claims

  http POST http://localhost:8080/dynamic-builder-general claim-1=value-1 ... [claim-n=value-n]
    build JWT from passed in claims (using general claims map)

  http POST http://localhost:8080/dynamic-builder-specific claim-1=value-1 ... [claim-n=value-n]
    build JWT from passed in claims (using specific claims methods)

  http POST http://localhost:8080/dynamic-builder-compress claim-1=value-1 ... [claim-n=value-n]
    build DEFLATE compressed JWT from passed in claims

  http http://localhost:8080/parser?jwt=
    Parse passed in JWT

  http http://localhost:8080/parser-enforce?jwt=
    Parse passed in JWT enforcing the 'iss' registered claim and the 'hasMotorcycle' custom claim

  http http://localhost:8080/get-secrets
    Show the signing keys currently in use.

  http http://localhost:8080/refresh-secrets
    Generate new signing keys and show them.

  http POST http://localhost:8080/set-secrets
    HS256=base64-encoded-value HS384=base64-encoded-value HS512=base64-encoded-value
    Explicitly set secrets to use in the application.

В следующих разделах мы рассмотрим каждую из этих конечных точек и код JJWT, содержащийся в обработчиках.

4. Создание JWT с помощью JJWT

Из-заfluent interface JJWT создание JWT в основном состоит из трех этапов:

  1. Определение внутренних утверждений токена, таких как эмитент, тема, срок действия и идентификатор.

  2. Криптографическое подписание JWT (что делает его JWS).

  3. Сжатие JWT в URL-безопасную строку в соответствии с правиламиJWT Compact Serialization.

Конечный JWT будет трехчастной строкой в ​​кодировке base64, подписанной указанным алгоритмом подписи и использующим предоставленный ключ. После этого токен готов к передаче другой стороне.

Вот пример JJWT в действии:

String jws = Jwts.builder()
  .setIssuer("Stormpath")
  .setSubject("msilverman")
  .claim("name", "Micah Silverman")
  .claim("scope", "admins")
  // Fri Jun 24 2016 15:33:42 GMT-0400 (EDT)
  .setIssuedAt(Date.from(Instant.ofEpochSecond(1466796822L)))
  // Sat Jun 24 2116 15:33:42 GMT-0400 (EDT)
  .setExpiration(Date.from(Instant.ofEpochSecond(4622470422L)))
  .signWith(
    SignatureAlgorithm.HS256,
    TextCodec.BASE64.decode("Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E=")
  )
  .compact();

Это очень похоже на код в методеStaticJWTController.fixedBuilder проекта кода.

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

  1. .signWith(
        SignatureAlgorithm.HS256,
       "secret".getBytes("UTF-8")
    )
  2. .signWith(
        SignatureAlgorithm.HS256,
        "Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E=".getBytes("UTF-8")
    )
  3. .signWith(
        SignatureAlgorithm.HS512,
        TextCodec.BASE64.decode("Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E=")
    )

Любой из алгоритмов подписи типаHS принимает байтовый массив. Людям удобно читать, чтобы взять строку и преобразовать ее в массив байтов.

Анти-шаблон 1 выше демонстрирует это. Это проблематично, потому что секрет ослабляется из-за того, что он такой короткий, и это не массив байтов в его исходной форме. Таким образом, чтобы сделать его читаемым, мы можем base64-кодировать байтовый массив.

Однако вышеприведенный анти-шаблон 2 берет строку в кодировке base64 и преобразует ее непосредственно в байтовый массив. Что нужно сделать, это декодировать строку base64 обратно в исходный байтовый массив.

Номер 3 выше демонстрирует это. Так почему же этот анти-паттерн? В данном случае это тонкая причина. Обратите внимание, что алгоритм подписи - HS512. Массив байтов не является максимальной длиной, которую может поддерживатьHS512, что делает его более слабым секретом, чем то, что возможно для этого алгоритма.

Код примера включает класс под названиемSecretService, который гарантирует, что секреты надлежащей силы используются для данного алгоритма. Во время запуска приложения создается новый набор секретов для каждого из алгоритмов HS. Существуют конечные точки для обновления секретов, а также для явной установки секретов.

Если ваш проект работает так, как описано выше, выполните следующее, чтобы приведенные ниже примеры JWT соответствовали ответам вашего проекта.

http POST localhost:8080/set-secrets \
  HS256="Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E=" \
  HS384="VW96zL+tYlrJLNCQ0j6QPTp+d1q75n/Wa8LVvpWyG8pPZOP6AA5X7XOIlI90sDwx" \
  HS512="cd+Pr1js+w2qfT2BoCD+tPcYp9LbjpmhSMEJqUob1mcxZ7+Wmik4AYdjX+DlDjmE4yporzQ9tm7v3z/j+QbdYg=="

Теперь вы можете попасть в конечную точку/static-builder:

http http://localhost:8080/static-builder

Это создает JWT, который выглядит следующим образом:

eyJhbGciOiJIUzI1NiJ9.
eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIiwibmFtZSI6Ik1pY2FoIFNpbHZlcm1hbiIsInNjb3BlIjoiYWRtaW5zIiwiaWF0IjoxNDY2Nzk2ODIyLCJleHAiOjQ2MjI0NzA0MjJ9.
kP0i_RvTAmI8mgpIkDFhRX3XthSdP-eqqFKGcU92ZIQ

Теперь нажмите:

http http://localhost:8080/parser?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIiwibmFtZSI6Ik1pY2FoIFNpbHZlcm1hbiIsInNjb3BlIjoiYWRtaW5zIiwiaWF0IjoxNDY2Nzk2ODIyLCJleHAiOjQ2MjI0NzA0MjJ9.kP0i_RvTAmI8mgpIkDFhRX3XthSdP-eqqFKGcU92ZIQ

Ответ содержит все утверждения, которые мы включили при создании JWT.

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
...
{
    "jws": {
        "body": {
            "exp": 4622470422,
            "iat": 1466796822,
            "iss": "Stormpath",
            "name": "Micah Silverman",
            "scope": "admins",
            "sub": "msilverman"
        },
        "header": {
            "alg": "HS256"
        },
        "signature": "kP0i_RvTAmI8mgpIkDFhRX3XthSdP-eqqFKGcU92ZIQ"
    },
    "status": "SUCCESS"
}

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

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

http -v POST localhost:8080/dynamic-builder-general iss=Stormpath sub=msilverman hasMotorcycle:=true

Note: есть небольшая разница между заявкойhasMotorcycle и другими заявками. httpie предполагает, что параметры JSON являются строками по умолчанию. Чтобы отправить необработанный JSON с помощью httpie, вы используете форму:=, а не=. Без этого он отправил бы“hasMotorcycle”: “true”, чего мы не хотим.

Вот результат:

POST /dynamic-builder-general HTTP/1.1
Accept: application/json
...
{
    "hasMotorcycle": true,
    "iss": "Stormpath",
    "sub": "msilverman"
}

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
...
{
    "jwt":
      "eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIiwiaGFzTW90b3JjeWNsZSI6dHJ1ZX0.OnyDs-zoL3-rw1GaSl_KzZzHK9GoiNocu-YwZ_nQNZU",
    "status": "SUCCESS"
}

Давайте посмотрим на код, поддерживающий эту конечную точку:

@RequestMapping(value = "/dynamic-builder-general", method = POST)
public JwtResponse dynamicBuilderGeneric(@RequestBody Map claims)
  throws UnsupportedEncodingException {
    String jws =  Jwts.builder()
        .setClaims(claims)
        .signWith(
            SignatureAlgorithm.HS256,
            secretService.getHS256SecretBytes()
        )
        .compact();
    return new JwtResponse(jws);
}

Строка 2 гарантирует, что входящий JSON автоматически преобразуется в карту Java , что очень удобно для JJWT, поскольку метод в строке 5 просто берет эту карту и устанавливает все утверждения сразу.

Каким бы кратким ни был этот код, нам нужно нечто более конкретное, чтобы убедиться, что переданные утверждения действительны. Использование метода.setClaims(Map<String, Object> claims) удобно, когда вы уже знаете, что утверждения, представленные на карте, действительны. Здесь безопасность типов Java входит в библиотеку JJWT.

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

Давайте обратимся к другой конечной точке в нашем примере и посмотрим, что произойдет:

http -v POST localhost:8080/dynamic-builder-specific iss=Stormpath sub:=5 hasMotorcycle:=true

Обратите внимание, что для дополнительного утверждения мы передали целое число 5. Вот результат:

POST /dynamic-builder-specific HTTP/1.1
Accept: application/json
...
{
    "hasMotorcycle": true,
    "iss": "Stormpath",
    "sub": 5
}

HTTP/1.1 400 Bad Request
Connection: close
Content-Type: application/json;charset=UTF-8
...
{
    "exceptionType": "java.lang.ClassCastException",
    "message": "java.lang.Integer cannot be cast to java.lang.String",
    "status": "ERROR"
}

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

@RequestMapping(value = "/dynamic-builder-specific", method = POST)
public JwtResponse dynamicBuilderSpecific(@RequestBody Map claims)
  throws UnsupportedEncodingException {
    JwtBuilder builder = Jwts.builder();

    claims.forEach((key, value) -> {
        switch (key) {
            case "iss":
                builder.setIssuer((String) value);
                break;
            case "sub":
                builder.setSubject((String) value);
                break;
            case "aud":
                builder.setAudience((String) value);
                break;
            case "exp":
                builder.setExpiration(Date.from(
                    Instant.ofEpochSecond(Long.parseLong(value.toString()))
                ));
                break;
            case "nbf":
                builder.setNotBefore(Date.from(
                    Instant.ofEpochSecond(Long.parseLong(value.toString()))
                ));
                break;
            case "iat":
                builder.setIssuedAt(Date.from(
                    Instant.ofEpochSecond(Long.parseLong(value.toString()))
                ));
                break;
            case "jti":
                builder.setId((String) value);
                break;
            default:
                builder.claim(key, value);
        }
    });

    builder.signWith(SignatureAlgorithm.HS256, secretService.getHS256SecretBytes());

    return new JwtResponse(builder.compact());
}

Как и раньше, метод принимает в качестве параметраMap<String, Object> заявок. Однако на этот раз мы вызываем конкретный метод для каждого из зарегистрированных утверждений, который устанавливает тип.

Одним из уточнений этого является уточнение сообщения об ошибке. Прямо сейчас мы знаем только, что одно из наших утверждений неверного типа. Мы не знаем, какая претензия была ошибочной и какой должна быть. Вот метод, который даст нам более конкретное сообщение об ошибке. Это также имеет дело с ошибкой в ​​текущем коде.

private void ensureType(String registeredClaim, Object value, Class expectedType) {
    boolean isCorrectType =
        expectedType.isInstance(value) ||
        expectedType == Long.class && value instanceof Integer;

    if (!isCorrectType) {
        String msg = "Expected type: " + expectedType.getCanonicalName() +
            " for registered claim: '" + registeredClaim + "', but got value: " +
            value + " of type: " + value.getClass().getCanonicalName();
        throw new JwtException(msg);
    }
}

Строка 3 проверяет, что переданное значение имеет ожидаемый тип. В противном случае выдаетсяJwtException с конкретной ошибкой. Давайте посмотрим на это в действии, сделав тот же вызов, который мы сделали ранее:

http -v POST localhost:8080/dynamic-builder-specific iss=Stormpath sub:=5 hasMotorcycle:=true
POST /dynamic-builder-specific HTTP/1.1
Accept: application/json
...
User-Agent: HTTPie/0.9.3

{
    "hasMotorcycle": true,
    "iss": "Stormpath",
    "sub": 5
}

HTTP/1.1 400 Bad Request
Connection: close
Content-Type: application/json;charset=UTF-8
...
{
    "exceptionType": "io.jsonwebtoken.JwtException",
    "message":
      "Expected type: java.lang.String for registered claim: 'sub', but got value: 5 of type: java.lang.Integer",
    "status": "ERROR"
}

Теперь у нас есть очень конкретное сообщение об ошибке, в котором говорится, что утверждениеsub ошибочно.

Вернемся к этой ошибке в нашем коде. Проблема не имеет ничего общего с библиотекой JJWT. Проблема в том, что преобразователь объектов JSON в Java, встроенный в Spring Boot, слишком умный для нашего же блага.

Если есть метод, который принимает объект Java, преобразователь JSON автоматически преобразует переданное число, которое меньше или равно 2 147 483 647 в JavaInteger. Точно так же он автоматически преобразует переданное число, превышающее 2 147 483 647, вLong Java. Для заявлений JWTiat,nbf иexp мы хотим, чтобы наш тест sureType проходил, независимо от того, является ли отображаемый объект целым или длинным. Вот почему у нас есть дополнительное предложение для определения того, является ли переданное значение правильным типом:

 boolean isCorrectType =
     expectedType.isInstance(value) ||
     expectedType == Long.class && value instanceof Integer;

Если мы ожидаем Long, но значение является экземпляром Integer, мы все равно говорим, что это правильный тип. Поняв, что происходит с этой проверкой, мы можем интегрировать ее в наш методdynamicBuilderSpecific:

@RequestMapping(value = "/dynamic-builder-specific", method = POST)
public JwtResponse dynamicBuilderSpecific(@RequestBody Map claims)
  throws UnsupportedEncodingException {
    JwtBuilder builder = Jwts.builder();

    claims.forEach((key, value) -> {
        switch (key) {
            case "iss":
                ensureType(key, value, String.class);
                builder.setIssuer((String) value);
                break;
            case "sub":
                ensureType(key, value, String.class);
                builder.setSubject((String) value);
                break;
            case "aud":
                ensureType(key, value, String.class);
                builder.setAudience((String) value);
                break;
            case "exp":
                ensureType(key, value, Long.class);
                builder.setExpiration(Date.from(
                    Instant.ofEpochSecond(Long.parseLong(value.toString()))
                ));
                break;
            case "nbf":
                ensureType(key, value, Long.class);
                builder.setNotBefore(Date.from(
                    Instant.ofEpochSecond(Long.parseLong(value.toString()))
                ));
                break;
            case "iat":
                ensureType(key, value, Long.class);
                builder.setIssuedAt(Date.from(
                    Instant.ofEpochSecond(Long.parseLong(value.toString()))
                ));
                break;
            case "jti":
                ensureType(key, value, String.class);
                builder.setId((String) value);
                break;
            default:
                builder.claim(key, value);
        }
    });

    builder.signWith(SignatureAlgorithm.HS256, secretService.getHS256SecretBytes());

    return new JwtResponse(builder.compact());
}

Note: Во всех примерах кода в этом разделе JWT подписываются HMAC с использованием алгоритма SHA-256. Это сделано для того, чтобы примеры были простыми. Библиотека JJWT поддерживает 12 различных алгоритмов подписи, которые вы можете использовать в своем собственном коде.

5. Разбор JWT с помощью JJWT

Ранее мы видели, что в нашем примере кода есть конечная точка для анализа JWT. Достижение этой конечной точки:

http http://localhost:8080/parser?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIiwibmFtZSI6Ik1pY2FoIFNpbHZlcm1hbiIsInNjb3BlIjoiYWRtaW5zIiwiaWF0IjoxNDY2Nzk2ODIyLCJleHAiOjQ2MjI0NzA0MjJ9.kP0i_RvTAmI8mgpIkDFhRX3XthSdP-eqqFKGcU92ZIQ

производит этот ответ:

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
...
{
    "claims": {
        "body": {
            "exp": 4622470422,
            "iat": 1466796822,
            "iss": "Stormpath",
            "name": "Micah Silverman",
            "scope": "admins",
            "sub": "msilverman"
        },
        "header": {
            "alg": "HS256"
        },
        "signature": "kP0i_RvTAmI8mgpIkDFhRX3XthSdP-eqqFKGcU92ZIQ"
    },
    "status": "SUCCESS"
}

Методparser классаStaticJWTController выглядит так:

@RequestMapping(value = "/parser", method = GET)
public JwtResponse parser(@RequestParam String jwt) throws UnsupportedEncodingException {
    Jws jws = Jwts.parser()
        .setSigningKeyResolver(secretService.getSigningKeyResolver())
        .parseClaimsJws(jwt);

    return new JwtResponse(jws);
}

Строка 4 указывает, что мы ожидаем, что входящая строка будет JWT со знаком (JWS). И мы используем тот же секрет, который использовался для подписи JWT при его анализе. Строка 5 анализирует претензии JWT. Внутренне он проверяет подпись и выдает исключение, если подпись недействительна.

Обратите внимание, что в этом случае мы передаемSigningKeyResolver, а не сам ключ. Это один из самых мощных аспектов JJWT. Заголовок JWT указывает алгоритм, используемый для его подписи. Тем не менее, мы должны проверить JWT, прежде чем доверять ему. Казалось бы, подвох 22. Давайте посмотрим на методSecretService.getSigningKeyResolver:

private SigningKeyResolver signingKeyResolver = new SigningKeyResolverAdapter() {
    @Override
    public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) {
        return TextCodec.BASE64.decode(secrets.get(header.getAlgorithm()));
    }
};

Используя доступ кJwsHeader, я могу проверить алгоритм и вернуть правильный массив байтов для секрета, который использовался для подписи JWT. Теперь JJWT проверит, что JWT не был подделан с использованием этого байтового массива в качестве ключа.

Если я удаляю последний символ переданного в JWT (который является частью подписи), это ответ:

HTTP/1.1 400 Bad Request
Connection: close
Content-Type: application/json;charset=UTF-8
Date: Mon, 27 Jun 2016 13:19:08 GMT
Server: Apache-Coyote/1.1
Transfer-Encoding: chunked

{
    "exceptionType": "io.jsonwebtoken.SignatureException",
    "message":
      "JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.",
    "status": "ERROR"
}

6. JWT на практике: токены CSRF Spring Security

Хотя этот пост посвящен не Spring Security, мы немного углубимся в него, чтобы продемонстрировать некоторые примеры использования библиотеки JJWT в реальных условиях.

Cross Site Request Forgery - это уязвимость системы безопасности, с помощью которой вредоносный веб-сайт обманом заставляет вас отправлять запросы на веб-сайт, которому вы установили доверие. Одним из распространенных способов решения этой проблемы является реализацияsynchronizer token pattern. При таком подходе токен вставляется в веб-форму, а сервер приложений проверяет входящий токен в своем хранилище, чтобы подтвердить его правильность. Если токен отсутствует или недействителен, сервер ответит ошибкой.

Spring Security имеет встроенный шаблон токена синхронизатора. Еще лучше, если вы используетеSpring Boot and Thymeleaf templates, токен синхронизатора вставляется автоматически.

По умолчанию токен, который использует Spring Security, является «тупым» токеном. Это просто набор букв и цифр. Этот подход просто отлично, и он работает. В этом разделе мы расширяем базовую функциональность, используя JWT в качестве токена. В дополнение к проверке того, что представленный токен является ожидаемым, мы проверяем JWT, чтобы дополнительно доказать, что токен не был подделан, и убедиться, что срок его действия не истек.

Для начала мы собираемся настроить Spring Security с помощью конфигурации Java. По умолчанию все пути требуют аутентификации, а все конечные точки POST требуют токенов CSRF. Мы собираемся немного расслабиться, чтобы то, что мы создали, все еще работало.

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private String[] ignoreCsrfAntMatchers = {
        "/dynamic-builder-compress",
        "/dynamic-builder-general",
        "/dynamic-builder-specific",
        "/set-secrets"
    };

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf()
                .ignoringAntMatchers(ignoreCsrfAntMatchers)
            .and().authorizeRequests()
                .antMatchers("/**")
                .permitAll();
    }
}

Мы делаем две вещи здесь. Во-первых, мы говорим, что токены CSRF являютсяnot, необходимыми при публикации в конечные точки REST API (строка 15). Во-вторых, мы говорим, что доступ без аутентификации должен быть разрешен для всех путей (строки 17-18).

Подтверждаем, что Spring Security работает так, как мы ожидали. Запустите приложение и нажмите этот URL в вашем браузере:

http://localhost:8080/jwt-csrf-form

Вот шаблон Thymeleaf для этого представления:



    
        
    
    
        

Это очень простая форма, которая будет отправлена ​​в тот же конечный пункт при отправке. Обратите внимание, что в форме нет явной ссылки на токены CSRF. Если вы посмотрите на источник, вы увидите что-то вроде:

Это все подтверждение того, что вам нужно знать, что Spring Security работает и что шаблоны Thymeleaf автоматически вставляют токен CSRF.

Чтобы сделать значение JWT, мы включим пользовательскийCsrfTokenRepository. Вот как меняется наша конфигурация Spring Security:

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    CsrfTokenRepository jwtCsrfTokenRepository;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf()
                .csrfTokenRepository(jwtCsrfTokenRepository)
                .ignoringAntMatchers(ignoreCsrfAntMatchers)
            .and().authorizeRequests()
                .antMatchers("/**")
                .permitAll();
    }
}

Чтобы соединить это, нам нужна конфигурация, которая предоставляет компонент, который возвращает хранилище пользовательских токенов. Вот конфигурация:

@Configuration
public class CSRFConfig {

    @Autowired
    SecretService secretService;

    @Bean
    @ConditionalOnMissingBean
    public CsrfTokenRepository jwtCsrfTokenRepository() {
        return new JWTCsrfTokenRepository(secretService.getHS256SecretBytes());
    }
}

И вот наш собственный репозиторий (важные части):

public class JWTCsrfTokenRepository implements CsrfTokenRepository {

    private static final Logger log = LoggerFactory.getLogger(JWTCsrfTokenRepository.class);
    private byte[] secret;

    public JWTCsrfTokenRepository(byte[] secret) {
        this.secret = secret;
    }

    @Override
    public CsrfToken generateToken(HttpServletRequest request) {
        String id = UUID.randomUUID().toString().replace("-", "");

        Date now = new Date();
        Date exp = new Date(System.currentTimeMillis() + (1000*30)); // 30 seconds

        String token;
        try {
            token = Jwts.builder()
                .setId(id)
                .setIssuedAt(now)
                .setNotBefore(now)
                .setExpiration(exp)
                .signWith(SignatureAlgorithm.HS256, secret)
                .compact();
        } catch (UnsupportedEncodingException e) {
            log.error("Unable to create CSRf JWT: {}", e.getMessage(), e);
            token = id;
        }

        return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", token);
    }

    @Override
    public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
        ...
    }

    @Override
    public CsrfToken loadToken(HttpServletRequest request) {
        ...
    }
}

МетодgenerateToken создает JWT, срок действия которого истекает через 30 секунд после его создания. Имея это соединение, мы можем снова запустить приложение и посмотреть на источник/jwt-csrf-form.

Теперь скрытое поле выглядит так:

Ура! Теперь наш токен CSRF является JWT. Это было не так уж сложно.

Однако это только половина загадки. По умолчанию Spring Security просто сохраняет токен CSRF и подтверждает, что токен, отправленный в веб-форме, совпадает с сохраненным. Мы хотим расширить функциональность, чтобы проверить JWT и убедиться, что срок его действия не истек. Для этого мы добавим фильтр. Вот как теперь выглядит наша конфигурация Spring Security:

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    ...

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .addFilterAfter(new JwtCsrfValidatorFilter(), CsrfFilter.class)
            .csrf()
                .csrfTokenRepository(jwtCsrfTokenRepository)
                .ignoringAntMatchers(ignoreCsrfAntMatchers)
            .and().authorizeRequests()
                .antMatchers("/**")
                .permitAll();
    }

    ...
}

В строке 9 мы добавили фильтр и помещаем его в цепочку фильтров после значения по умолчаниюCsrfFilter. Таким образом, к моменту срабатывания нашего фильтра токен JWT (в целом) уже будет подтвержден как правильное значение, сохраненное Spring Security.

ВотJwtCsrfValidatorFilter (он частный, так как это внутренний класс нашей конфигурации Spring Security):

private class JwtCsrfValidatorFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(
      HttpServletRequest request,
      HttpServletResponse response,
      FilterChain filterChain) throws ServletException, IOException {
        // NOTE: A real implementation should have a nonce cache so the token cannot be reused
        CsrfToken token = (CsrfToken) request.getAttribute("_csrf");

        if (
            // only care if it's a POST
            "POST".equals(request.getMethod()) &&
            // ignore if the request path is in our list
            Arrays.binarySearch(ignoreCsrfAntMatchers, request.getServletPath()) < 0 &&
            // make sure we have a token
            token != null
        ) {
            // CsrfFilter already made sure the token matched.
            // Here, we'll make sure it's not expired
            try {
                Jwts.parser()
                    .setSigningKey(secret.getBytes("UTF-8"))
                    .parseClaimsJws(token.getToken());
            } catch (JwtException e) {
                // most likely an ExpiredJwtException, but this will handle any
                request.setAttribute("exception", e);
                response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
                RequestDispatcher dispatcher = request.getRequestDispatcher("expired-jwt");
                dispatcher.forward(request, response);
            }
        }

        filterChain.doFilter(request, response);
    }
}

Посмотрите на строку 23 на. Мы разбираем JWT, как и раньше. В этом случае, если генерируется исключение, запрос перенаправляется в шаблонexpired-jwt. Если JWT подтверждает, обработка продолжается как обычно.

Это завершает цикл переопределения поведения токена Spring Security CSRF по умолчанию с хранилищем токена JWT и средством проверки.

Если вы запустите приложение, перейдите к/jwt-csrf-form, подождите чуть более 30 секунд и нажмите кнопку, вы увидите что-то вроде этого:

jwt_expired

7. Расширенные возможности JJWT

Мы завершим наше путешествие по JJWT несколькими словами о некоторых функциях, выходящих за рамки спецификации.

7.1. Обеспечить соблюдение требований

Как часть процесса синтаксического анализа, JJWT позволяет вам указать требуемые утверждения и значения, которые должны иметь эти утверждения. Это очень удобно, если в ваших JWT есть определенная информация, которая должна присутствовать, чтобы вы считали их действительными. Это позволяет избежать большого количества логики ветвления для ручной проверки утверждений. Вот метод, который обслуживает конечную точку/parser-enforce в нашем примере проекта.

@RequestMapping(value = "/parser-enforce", method = GET)
public JwtResponse parserEnforce(@RequestParam String jwt)
  throws UnsupportedEncodingException {
    Jws jws = Jwts.parser()
        .requireIssuer("Stormpath")
        .require("hasMotorcycle", true)
        .setSigningKeyResolver(secretService.getSigningKeyResolver())
        .parseClaimsJws(jwt);

    return new JwtResponse(jws);
}

В строках 5 и 6 показан синтаксис зарегистрированных заявок, а также пользовательских заявок. В этом примере JWT будет считаться недействительным, если утверждение iss отсутствует или не имеет значения: Stormpath. Он также будет недействительным, если пользовательское утверждение hasMotorcycle отсутствует или не имеет значения: true.

Давайте сначала создадим JWT, который пойдет по счастливому пути:

http -v POST localhost:8080/dynamic-builder-specific \
  iss=Stormpath hasMotorcycle:=true sub=msilverman
POST /dynamic-builder-specific HTTP/1.1
Accept: application/json
...
{
    "hasMotorcycle": true,
    "iss": "Stormpath",
    "sub": "msilverman"
}

HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Type: application/json;charset=UTF-8
...
{
    "jwt":
      "eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJoYXNNb3RvcmN5Y2xlIjp0cnVlLCJzdWIiOiJtc2lsdmVybWFuIn0.qrH-U6TLSVlHkZdYuqPRDtgKNr1RilFYQJtJbcgwhR0",
    "status": "SUCCESS"
}

Теперь давайте проверим этот JWT:

http -v localhost:8080/parser-enforce?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJoYXNNb3RvcmN5Y2xlIjp0cnVlLCJzdWIiOiJtc2lsdmVybWFuIn0.qrH-U6TLSVlHkZdYuqPRDtgKNr1RilFYQJtJbcgwhR0
GET /parser-enforce?jwt=http
  -v localhost:8080/parser-enforce?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJoYXNNb3RvcmN5Y2xlIjp0cnVlLCJzdWIiOiJtc2lsdmVybWFuIn0.qrH-U6TLSVlHkZdYuqPRDtgKNr1RilFYQJtJbcgwhR0 HTTP/1.1
Accept: */*
...
HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Type: application/json;charset=UTF-8
...
{
    "jws": {
        "body": {
            "hasMotorcycle": true,
            "iss": "Stormpath",
            "sub": "msilverman"
        },
        "header": {
            "alg": "HS256"
        },
        "signature": "qrH-U6TLSVlHkZdYuqPRDtgKNr1RilFYQJtJbcgwhR0"
    },
    "status": "SUCCESS"
}

Все идет нормально. Теперь, на этот раз, оставим hasMotorcycle в стороне:

http -v POST localhost:8080/dynamic-builder-specific iss=Stormpath sub=msilverman

На этот раз, если мы попытаемся проверить JWT:

http -v localhost:8080/parser-enforce?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIn0.YMONlFM1tNgttUYukDRsi9gKIocxdGAOLaJBymaQAWc

мы получаем:

GET /parser-enforce?jwt=http -v localhost:8080/parser-enforce?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIn0.YMONlFM1tNgttUYukDRsi9gKIocxdGAOLaJBymaQAWc HTTP/1.1
Accept: */*
...
HTTP/1.1 400 Bad Request
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Connection: close
Content-Type: application/json;charset=UTF-8
...
{
    "exceptionType": "io.jsonwebtoken.MissingClaimException",
    "message":
      "Expected hasMotorcycle claim to be: true, but was not present in the JWT claims.",
    "status": "ERROR"
}

Это указывает на то, что наше требование hasMotorcycle было ожидаемым, но отсутствовало.

Приведем еще один пример:

http -v POST localhost:8080/dynamic-builder-specific iss=Stormpath hasMotorcycle:=false sub=msilverman

На этот раз требуемое требование присутствует, но оно имеет неправильное значение. Посмотрим на результат:

http -v localhost:8080/parser-enforce?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJoYXNNb3RvcmN5Y2xlIjpmYWxzZSwic3ViIjoibXNpbHZlcm1hbiJ9.8LBq2f0eINB34AzhVEgsln_KDo-IyeM8kc-dTzSCr0c
GET /parser-enforce?jwt=http
  -v localhost:8080/parser-enforce?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJoYXNNb3RvcmN5Y2xlIjpmYWxzZSwic3ViIjoibXNpbHZlcm1hbiJ9.8LBq2f0eINB34AzhVEgsln_KDo-IyeM8kc-dTzSCr0c HTTP/1.1
Accept: */*
...
HTTP/1.1 400 Bad Request
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Connection: close
Content-Type: application/json;charset=UTF-8
...
{
    "exceptionType": "io.jsonwebtoken.IncorrectClaimException",
    "message": "Expected hasMotorcycle claim to be: true, but was: false.",
    "status": "ERROR"
}

Это указывает на то, что наша заявка hasMotorcycle присутствовала, но имела значение, которое не ожидалось.

MissingClaimException иIncorrectClaimException - ваши друзья при принудительном применении требований в ваших JWT и функции, которая есть только в библиотеке JJWT.

7.2. Сжатие JWT

Если у вас много претензий к JWT, он может стать большим - настолько большим, что в некоторых браузерах он может не соответствовать URL-адресу GET.

Давайте сделаем большой JWT:

http -v POST localhost:8080/dynamic-builder-specific \
  iss=Stormpath hasMotorcycle:=true sub=msilverman the=quick brown=fox jumped=over lazy=dog \
  somewhere=over rainbow=way up=high and=the dreams=you dreamed=of

Вот JWT, который производит:

eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJoYXNNb3RvcmN5Y2xlIjp0cnVlLCJzdWIiOiJtc2lsdmVybWFuIiwidGhlIjoicXVpY2siLCJicm93biI6ImZveCIsImp1bXBlZCI6Im92ZXIiLCJsYXp5IjoiZG9nIiwic29tZXdoZXJlIjoib3ZlciIsInJhaW5ib3ciOiJ3YXkiLCJ1cCI6ImhpZ2giLCJhbmQiOiJ0aGUiLCJkcmVhbXMiOiJ5b3UiLCJkcmVhbWVkIjoib2YifQ.AHNJxSTiDw_bWNXcuh-LtPLvSjJqwDvOOUcmkk7CyZA

Этот сосунок большой! А теперь давайте обратимся к немного другой конечной точке с теми же утверждениями:

http -v POST localhost:8080/dynamic-builder-compress \
  iss=Stormpath hasMotorcycle:=true sub=msilverman the=quick brown=fox jumped=over lazy=dog \
  somewhere=over rainbow=way up=high and=the dreams=you dreamed=of

На этот раз мы получаем:

eyJhbGciOiJIUzI1NiIsImNhbGciOiJERUYifQ.eNpEzkESwjAIBdC7sO4JegdXnoC2tIk2oZLEGB3v7s84jjse_AFe5FOikc5ZLRycHQ3kOJ0Untu8C43ZigyUyoRYSH6_iwWOyGWHKd2Kn6_QZFojvOoDupRwyAIq4vDOzwYtugFJg1QnJv-5sY-TVjQqN7gcKJ3f-j8c-6J-baDFhEN_uGn58XtnpfcHAAD__w.3_wc-2skFBbInk0YAQ96yGWwr8r1xVdbHn-uGPTFuFE

На 62 символа короче! Вот код метода, используемого для создания JWT:

@RequestMapping(value = "/dynamic-builder-compress", method = POST)
public JwtResponse dynamicBuildercompress(@RequestBody Map claims)
  throws UnsupportedEncodingException {
    String jws =  Jwts.builder()
        .setClaims(claims)
        .compressWith(CompressionCodecs.DEFLATE)
        .signWith(
            SignatureAlgorithm.HS256,
            secretService.getHS256SecretBytes()
        )
        .compact();
    return new JwtResponse(jws);
}

Обратите внимание, что в строке 6 мы указываем используемый алгоритм сжатия. Это все, что нужно сделать.

Как насчет разбора сжатых JWT? Библиотека JJWT автоматически обнаруживает сжатие и использует тот же алгоритм для распаковки:

GET /parser?jwt=eyJhbGciOiJIUzI1NiIsImNhbGciOiJERUYifQ.eNpEzkESwjAIBdC7sO4JegdXnoC2tIk2oZLEGB3v7s84jjse_AFe5FOikc5ZLRycHQ3kOJ0Untu8C43ZigyUyoRYSH6_iwWOyGWHKd2Kn6_QZFojvOoDupRwyAIq4vDOzwYtugFJg1QnJv-5sY-TVjQqN7gcKJ3f-j8c-6J-baDFhEN_uGn58XtnpfcHAAD__w.3_wc-2skFBbInk0YAQ96yGWwr8r1xVdbHn-uGPTFuFE HTTP/1.1
Accept: */*
...
HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Type: application/json;charset=UTF-8
...
{
    "claims": {
        "body": {
            "and": "the",
            "brown": "fox",
            "dreamed": "of",
            "dreams": "you",
            "hasMotorcycle": true,
            "iss": "Stormpath",
            "jumped": "over",
            "lazy": "dog",
            "rainbow": "way",
            "somewhere": "over",
            "sub": "msilverman",
            "the": "quick",
            "up": "high"
        },
        "header": {
            "alg": "HS256",
            "calg": "DEF"
        },
        "signature": "3_wc-2skFBbInk0YAQ96yGWwr8r1xVdbHn-uGPTFuFE"
    },
    "status": "SUCCESS"
}

Обратите внимание на утверждениеcalg в заголовке. Это было автоматически закодировано в JWT и предоставляет подсказку анализатору о том, какой алгоритм использовать для распаковки.

Note
Спецификация JWE поддерживает сжатие. В следующем выпуске библиотеки JJWT мы будем поддерживать JWE и сжатые JWE. Мы продолжим поддерживать сжатие в других типах JWT, даже если это не указано.

8. Инструменты токенов для разработчиков Java

Хотя основной темой этой статьи не было Spring Boot или Spring Security, использование этих двух технологий позволило легко продемонстрировать все функции, обсуждаемые в этой статье. Вы должны иметь возможность встроить сервер и начать играть с различными конечными точками, которые мы обсуждали. Просто нажмите:

http http://localhost:8080

Stormpath также рад представить сообществу Java ряд инструментов разработчика с открытым исходным кодом. Они включают:

8.1. JJWT (о чем мы говорили)

JJWT - это простой в использованииtool for developers to create and verify JWTs in Java. Как и многие библиотеки, поддерживаемые Stormpath, JJWT является полностью бесплатным и открытым исходным кодом (Apache License, версия 2.0), поэтому каждый может увидеть, что он делает и как он это делает. Не стесняйтесь сообщать о любых проблемах, предлагать улучшения и даже отправлять код!

8.2. jsonwebtoken.io and java.jsonwebtoken.io

jsonwebtoken.io - это инструмент разработчика, который мы создали, чтобы упростить декодирование JWT. Просто вставьте существующий JWT в соответствующее поле, чтобы декодировать его заголовок, полезную нагрузку и подпись. jsonwebtoken.io is powered by nJWT, the cleanest free and open source (Apache License, Version 2.0) JWT library for Node.js developers. Вы также можете увидеть код, сгенерированный для разных языков на этом сайте. Сам веб-сайт имеет открытый исходный код, и его можно найти наhere.

java.jsonwebtoken.io специально для библиотеки JJWT. Вы можете изменить заголовки и полезную нагрузку в верхнем правом окне, посмотреть JWT, сгенерированный JJWT в верхнем левом окне, и увидеть образец Java-кода компоновщика и анализатора в нижних полях. Сам сайт с открытым исходным кодом, его можно найтиhere.

8.3. Инспектор JWT

Новый ребенок в блоке,JWT Inspector - это расширение Chrome с открытым исходным кодом, которое позволяет разработчикам проверять и отлаживать JWT прямо в браузере. Инспектор JWT обнаружит JWT на вашем сайте (в файлах cookie, локальном / сессионном хранилище и заголовках) и сделает их легко доступными через панель навигации и панель DevTools.

9. JWT это вниз!

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

В Stormpath мы используем JWT для токенов OAuth2, CSRF-токенов и проверок между микросервисами, а также для других целей.

Как только вы начнете использовать JWT, вы никогда не сможете вернуться к тупым жетонам прошлого. Есть вопросы? Напишите мне@afitnerd в твиттере.