Статьи

Проверка подлинности токена для приложений Java

Управление идентификацией здания, включая аутентификацию и авторизацию? Попробуйте Stormpath! Наш REST API и надежная поддержка Java SDK могут устранить риск для безопасности и могут быть реализованы за считанные минуты. Зарегистрируйтесь и никогда больше не создавайте аутентификацию!

Обновление от 5.12.2016: Создание приложения Java? JJWT — это библиотека Java, обеспечивающая сквозное создание и проверку JWT, разработанная нашим собственным Les Hazlewood. Вечно бесплатная и с открытым исходным кодом (Apache License, версия 2.0), JJWT проста в использовании и понимании. Это было разработано с ориентированным на строителя беглым интерфейсом, скрывающим большую часть его сложности. Мы будем рады, если вы попробуете это , и дайте нам знать, что вы думаете! (И, если вы разработчик Node, проверьте NJWT !)

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

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

Аутентификация токена на помощь!

Давайте сначала рассмотрим, что мы подразумеваем под authentication и token в этом контексте.

Аутентификация доказывает, что пользователь — это тот, кем они себя называют.

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

Веб-токены JSON (JWT)

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

URL-безопасный — это причудливый способ сказать, что вся строка закодирована, поэтому в ней нет специальных символов, и токен может уместиться в URL.

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

Однако строка также может быть декодирована для извлечения мета-данных, а ее подпись может быть криптографически проверена, чтобы ваше приложение знало, что токен не был подделан.

JWT и OAuth2 токены доступа

Многие реализации OAuth2 используют JWT для своих токенов доступа. Следует отметить, что спецификации OAuth2 и JWT полностью отделены друг от друга и не зависят друг от друга. Использование JWT в качестве механизма токенов для OAuth2 дает много преимуществ, как мы увидим ниже.

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

В дикой природе они выглядят как еще одна уродливая строка:

1
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vdHJ1c3R5YXBwLmNvbS8iLCJleHAiOjEzMDA4MTkzODAsInN1YiI6InVzZXJzLzg5ODM0NjIiLCJzY29wZSI6InNlbGYgYXBpL2J1eSJ9.43DXvhrwMGeLLlP4P4izjgsBB2yrpo82oiUPhADakLs

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

1
2
3
4
5
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
.
eyJpc3MiOiJodHRwOi8vdHJ1c3R5YXBwLmNvbS8iLCJleHAiOjEzMDA4MTkzODAsInN1YiI6InVzZXJzLzg5ODM0NjIiLCJzY29wZSI6InNlbGYgYXBpL2J1eSJ9
.
43DXvhrwMGeLLlP4P4izjgsBB2yrpo82oiUPhADakLs

Структура JWT

JWT имеют структуру из трех частей, каждая из которых закодирована в base64:

jwt_parts

Вот расшифрованные части:

заголовок

1
2
3
4
{
  "typ": "JWT",
  "alg": "HS256"
}

требования

1
2
3
4
5
6
{
  "exp": 1300819380,
  "sub": "users/8983462",
  "scope": "self api/buy"
}

Криптографическая подпись

1
tß´—™à%O˜v+nî…SZu¯µ€U…8H×

Претензии JWT

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

iss это тот, кто выдал токен.
exp — это когда срок действия токена истекает.
sub является предметом токена. Обычно это какой-то идентификатор пользователя.

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

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

ПРИМЕЧАНИЕ. Для сервера по-прежнему крайне важно и всегда рекомендуется проверять действия, предпринимаемые клиентом. Если, например, на клиенте было предпринято какое-то административное действие, вы все равно хотели бы проверить на сервере приложений, что текущий пользователь имеет разрешение на выполнение этого действия. Вы никогда не будете полагаться только на информацию авторизации на стороне клиента.

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

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

Управление безопасностью токенов на предъявителя

Неявное доверие — это компромисс. Эти типы токенов часто называют токенами на предъявителя, потому что все, что требуется для получения доступа к защищенным разделам приложения, — это представление действительного, не просроченного токена.

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

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

Ява, JWT и Вы!

Мы очень гордимся проектом JJWT на Github. Первоначально созданный собственным техническим директором Stormpath, Les Hazlewood , это решение JWT с открытым исходным кодом для Java. Это самая простая в использовании и понимании библиотека для создания и проверки веб-токенов JSON в JVM.

Как вы создаете JWT? Очень просто!

01
02
03
04
05
06
07
08
09
10
11
12
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
  
byte[] key = getSignatureKey();
  
String jwt =
    Jwts.builder().setIssuer("http://trustyapp.com/")
        .setSubject("users/1300819380")
        .setExpiration(expirationDate)
        .put("scope", "self api/buy")
        .signWith(SignatureAlgorithm.HS256,key)
        .compact();

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

Также обратите внимание, что когда мы устанавливаем одну из претензий из спецификации, мы используем установщик. Например: .setSubject("users/1300819380") . Когда настраиваемая заявка установлена, мы используем вызов для указания и указания ключа и значения. Например: .put("scope", "self api/buy")

Так же просто проверить JWT.

01
02
03
04
05
06
07
08
09
10
11
12
13
String subject = "HACKER";
try {
    Jws jwtClaims =
        Jwts.parser().setSigningKey(key).parseClaimsJws(jwt);
  
    subject = claims.getBody().getSubject();
  
    //OK, we can trust this JWT
  
} catch (SignatureException e) {
  
    //don't trust the JWT!
}

Если JWT был каким-либо образом подделан, при анализе утверждений возникнет исключение SignatureException а значение переменной subject останется HACKER . Если это действительный JWT, то из него будет извлечен subject : claims.getBody().getSubject() : claims.getBody().getSubject()

Что такое OAuth?

В следующем разделе мы рассмотрим пример использования реализации OAuth2 в Stormpath, в которой используются JWT.

Существует много путаницы вокруг спецификации OAuth2. Отчасти это потому, что это действительно супер-спецификация — в ней много сложностей. Это также потому, что OAuth1.a и OAuth2 очень разные звери. Мы рассмотрим очень конкретное, простое в использовании подмножество спецификации OAuth2. У нас есть отличный пост, в котором более подробно рассказывается о том, что, черт возьми, такое OAuth . Здесь мы дадим краткий обзор и затем перейдем непосредственно к примерам.

OAuth2 — это протокол, который поддерживает рабочие процессы авторизации . Это означает, что это дает вам возможность убедиться, что у конкретного пользователя есть разрешения на что-либо.

Вот и все.

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

Просто помните, что OAuth2 — это протокол для авторизации .

Использование типов OAuth-грантов для авторизации

Давайте посмотрим на типичное взаимодействие OAuth2.

1
2
3
4
5
POST /oauth/token HTTP/1.1
Origin: https://foo.com
Content-Type: application/x-www-form-urlencoded
  
grant_type=password&username=username&password=password

grant_type требуется. Тип содержимого application/x-www-form-urlencoded необходим для этого типа взаимодействия. Учитывая, что вы передаете имя пользователя и пароль по сети, вы всегда хотели бы, чтобы соединение было безопасным. Хорошая вещь, однако, состоит в том, что ответ будет иметь токен-носитель OAuth2. Этот токен будет затем использоваться для каждого взаимодействия между браузером и сервером в будущем. Здесь очень краткая информация, где имя пользователя и пароль передаются по проводам. Предполагая, что служба аутентификации на сервере проверяет имя пользователя и пароль, вот ответ:

01
02
03
04
05
06
07
08
09
10
11
12
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
  
{
    "access_token":"2YotnFZFEjr1zCsicMWpAA...",
    "token_type":"example",
    "expires_in":3600,
    "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA...",
    "example_parameter":"example_value"
}

Обратите внимание на заголовки Cache-Control и Pragma . Мы не хотим, чтобы этот ответ где-либо кэшировался. access_token — это то, что будет использоваться браузером в последующих запросах. Опять же, нет прямой связи между OAuth2 и JWT. Однако access_token может быть JWT. Вот где появляется дополнительное преимущество закодированных метаданных. Вот как токен доступа используется в будущих запросах:

1
2
GET /admin HTTP/1.1
Authorization: Bearer 2YotnFZFEjr1zCsicMW...

Заголовок Authorization является стандартным заголовком. Для использования OAuth2 не требуется никаких пользовательских заголовков. Вместо того, чтобы быть типом Basic , в этом случае тип является Bearer . Маркер доступа включается непосредственно после ключевого слова Bearer . Это завершает взаимодействие OAuth2 для типа предоставления пароля. Каждый последующий запрос от браузера может использовать заголовок Authorizaion: Bearer с токеном доступа.

Есть другой тип предоставления, известный как client_credentials который использует client_id и client_secret , а не username и password . Этот тип предоставления обычно используется для взаимодействия API. Хотя идентификатор клиента и секретный секрет работают аналогично имени пользователя и паролю, они обычно имеют более высокий уровень безопасности и не обязательно читаются человеком.

Возьми нас домой: пример Java OAuth2

Мы прибыли! Пришло время покопаться в каком-то конкретном коде, который демонстрирует JWT в действии.

Spring Boot Web MVC

В Stormpath Java SDK есть несколько примеров. Здесь мы рассмотрим пример Spring Boot Web MVC. Вот HelloController из примера:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
@RestController
public class HelloController {
  
    @RequestMapping("/")
    String home(HttpServletRequest request) {
  
        String name = "World";
  
        Account account = AccountResolver.INSTANCE.getAccount(request);
        if (account != null) {
            name = account.getGivenName();
        }
  
        return "Hello " + name + "!";
    }
  
}

Ключевая линия для целей этой демонстрации:

Account account = AccountResolver.INSTANCE.getAccount(request);

За кулисами account будет преобразована в объект Account (и не будет null ), ТОЛЬКО если присутствует аутентифицированный сеанс.

Создайте и запустите пример кода

Чтобы построить и запустить этот пример, сделайте следующее:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
☺ dogeared jobs:0 ~/Projects/StormPath/stormpath-sdk-java (master|8100m)
cd examples/spring-boot-webmvc/
☺ dogeared jobs:0 ~/Projects/StormPath/stormpath-sdk-java/examples/spring-boot-webmvc (master|8100m)
➥ mvn clean package
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building Stormpath Java SDK :: Examples :: Spring Boot Webapp 1.0.RC4.6-SNAPSHOT
[INFO] ------------------------------------------------------------------------
  
... skipped output ...
  
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 4.865 s
[INFO] Finished at: 2015-08-04T11:46:05-04:00
[INFO] Final Memory: 31M/224M
[INFO] ------------------------------------------------------------------------
☺ dogeared jobs:0 ~/Projects/StormPath/stormpath-sdk-java/examples/spring-boot-webmvc (master|8100m

Запустите пример Spring Boot

Затем вы можете запустить пример Spring Boot следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
☺ dogeared jobs:0 ~/Projects/StormPath/stormpath-sdk-java/examples/spring-boot-webmvc (master|8104m)
➥ java -jar target/stormpath-sdk-examples-spring-boot-web-1.0.RC4.6-SNAPSHOT.jar
  
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.2.1.RELEASE)
  
2015-08-04 11:51:00.127  INFO 17973 --- [           main] tutorial.Application                     : Starting Application v1.0.RC4.6-SNAPSHOT on MacBook-Pro.local with PID 17973
  
... skipped output ...
  
2015-08-04 11:51:04.558  INFO 17973 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2015-08-04 11:51:04.559  INFO 17973 --- [           main] tutorial.Application                     : Started Application in 4.599 seconds (JVM running for 5.103)

ПРИМЕЧАНИЕ . Предполагается, что вы уже настроили учетную запись Stormpath и ваши ключи API находятся в ~/.stormpath/apiKey.properties . Посмотрите здесь для получения дополнительной информации о быстрой настройке Stormpath с Spring Boot.

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

Теперь мы можем продемонстрировать пример и показать некоторые JWT в действии! Во-первых, поразите свою конечную точку без какой-либо аутентификации. Мне нравится использовать httpie , но подойдет любой http-клиент командной строки.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
➥ http -v localhost:8080
GET / HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Host: localhost:8080
User-Agent: HTTPie/0.9.2
  
  
HTTP/1.1 200 OK
Accept-Charset: big5, big5-hkscs, cesu-8, euc-jp, euc-kr, gb18030, ...
Content-Length: 12
Content-Type: text/plain;charset=UTF-8
Date: Tue, 04 Aug 2015 15:56:41 GMT
Server: Apache-Coyote/1.1
  
Hello World!

Параметр -v производит подробный вывод и показывает все заголовки для запроса и ответа. В этом случае выводится сообщение просто: Hello World! , Это потому, что нет установленного сеанса.

Аутентификация с помощью конечной точки OAuth Stormpath

Теперь давайте oauth конечной точке oauth чтобы наш сервер мог проходить аутентификацию с помощью Stormpath. Вы можете спросить: «Какую oauth точку?» Контроллер выше не указывает ни одной такой конечной точки. Есть ли в этом примере другие контроллеры с другими конечными точками? Нет, здесь нету! Stormpath дает вам готовые (и многие другие) конечные точки прямо из коробки. Проверьте это:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
➥ http -v --form POST http://localhost:8080/oauth/token  \
> 'Origin:http://localhost:8080' \
> grant_type=password username=micah+demo.jsmith@stormpath.com password=
POST /oauth/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded; charset=utf-8
Host: localhost:8080
Origin: http://localhost:8080
User-Agent: HTTPie/0.9.2
  
grant_type=password&username=micah%2Bdemo.jsmith%40stormpath.com&password=
  
HTTP/1.1 200 OK
Cache-Control: no-store
Content-Length: 325
Content-Type: application/json;charset=UTF-8
Date: Tue, 04 Aug 2015 16:02:08 GMT
Pragma: no-cache
Server: Apache-Coyote/1.1
Set-Cookie: account=eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxNDQyNmQxMy1mNThiLTRhNDEtYmVkZS0wYjM0M2ZjZDFhYzAiLCJpYXQiOjE0Mzg3MDQxMjgsInN1YiI6Imh0dHBzOi8vYXBpLnN0b3JtcGF0aC5jb20vdjEvYWNjb3VudHMvNW9NNFdJM1A0eEl3cDRXaURiUmo4MCIsImV4cCI6MTQzODk2MzMyOH0.wcXrS5yGtUoewAKqoqL5JhIQ109s1FMNopL_50HR_t4; Expires=Wed, 05-Aug-2015 16:02:08 GMT; Path=/; HttpOnly
  
{
    "access_token": "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxNDQyNmQxMy1mNThiLTRhNDEtYmVkZS0wYjM0M2ZjZDFhYzAiLCJpYXQiOjE0Mzg3MDQxMjgsInN1YiI6Imh0dHBzOi8vYXBpLnN0b3JtcGF0aC5jb20vdjEvYWNjb3VudHMvNW9NNFdJM1A0eEl3cDRXaURiUmo4MCIsImV4cCI6MTQzODk2MzMyOH0.wcXrS5yGtUoewAKqoqL5JhIQ109s1FMNopL_50HR_t4",
    "expires_in": 259200,
    "token_type": "Bearer"
}

Здесь много чего происходит, поэтому давайте разберемся с этим.

В первой строке я говорю httpie что я хочу создать POST- --form в форме URL — это то, что --form параметры --form и POST . Я использую /oauth/token точку /oauth/token моего локально работающего сервера. Я указываю заголовок Origin . Это необходимо для взаимодействия с Stormpath по соображениям безопасности, о которых мы говорили ранее . Согласно спецификации OAuth2, я передаю grant_type=password вместе с username и password .

Ответ имеет заголовок Set-Cookie а также тело JSON, содержащее токен доступа OAuth2. И угадай что? Этот токен доступа также является JWT. Вот расшифрованные претензии:

1
2
3
4
5
6
{
  "jti": "14426d13-f58b-4a41-bede-0b343fcd1ac0",
  "iat": 1438704128,
  "exp": 1438963328
}

Обратите внимание на sub ключ. Это полный URL-адрес Stormpath для учетной записи, с которой я прошел аутентификацию. Теперь давайте снова перейдем к нашей базовой конечной точке Hello World, только на этот раз мы будем использовать токен доступа OAuth2:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
➥ http -v localhost:8080 \
> 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxNDQyNmQxMy1mNThiLTRhNDEtYmVkZS0wYjM0M2ZjZDFhYzAiLCJpYXQiOjE0Mzg3MDQxMjgsInN1YiI6Imh0dHBzOi8vYXBpLnN0b3JtcGF0aC5jb20vdjEvYWNjb3VudHMvNW9NNFdJM1A0eEl3cDRXaURiUmo4MCIsImV4cCI6MTQzODk2MzMyOH0.wcXrS5yGtUoewAKqoqL5JhIQ109s1FMNopL_50HR_t4'
GET / HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxNDQyNmQxMy1mNThiLTRhNDEtYmVkZS0wYjM0M2ZjZDFhYzAiLCJpYXQiOjE0Mzg3MDQxMjgsInN1YiI6Imh0dHBzOi8vYXBpLnN0b3JtcGF0aC5jb20vdjEvYWNjb3VudHMvNW9NNFdJM1A0eEl3cDRXaURiUmo4MCIsImV4cCI6MTQzODk2MzMyOH0.wcXrS5yGtUoewAKqoqL5JhIQ109s1FMNopL_50HR_t4
Connection: keep-alive
Host: localhost:8080
User-Agent: HTTPie/0.9.2
  
  
  
HTTP/1.1 200 OK
Content-Length: 11
Content-Type: text/plain;charset=UTF-8
Date: Tue, 04 Aug 2015 16:44:28 GMT
Server: Apache-Coyote/1.1
  
Hello John!

Обратите внимание на последнюю строку вывода, что сообщение обращается к нам по имени. Теперь, когда мы установили аутентифицированный сеанс с Stormpath, используя OAuth2, эти строки в контроллере получают имя:

1
2
3
4
Account account = AccountResolver.INSTANCE.getAccount(request);
if (account != null) {
    name = account.getGivenName();
}

Описание: аутентификация токена для приложений Java

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

Мы дали обзор протокола OAuth2 и рассмотрели подробный пример того, как реализация OAuth2 в Stormpath использует JWT.

Вот некоторые другие ссылки на сообщения об аутентификации на основе токенов, JWT и Spring Boot:

Управление идентификацией здания, включая аутентификацию и авторизацию? Попробуйте Stormpath! Наш REST API и надежная поддержка Java SDK могут устранить риск для безопасности и могут быть реализованы за считанные минуты. Зарегистрируйтесь и никогда больше не создавайте аутентификацию!