Аннотация
Я стал поклонником веб-токенов JSON ( JWT ) с тех пор, как обнаружил, что они могут предложить отличные решения для сложных требований к распределенному управлению доступом.
В течение последних пяти лет я использовал JWT, независимо или совместно с другими решениями безопасности, такими как SAML или OpenID Connect, для защиты многих распределенных веб-приложений.
В этой статье я хотел бы продемонстрировать, как JWT можно использовать для защиты доступа к микросервисам Java, созданным с помощью Spring Boot.
Содержание
- Реальная аналогия авторизации на основе токенов
- Технический обзор авторизации на основе JWT
- Семинар: единый вход для Spring Boot Microservices с JWT
- Изучение исходного кода
- Заключение
- Ссылки
Реальная аналогия авторизации на основе токенов
Я хотел бы привести мою любимую систему общественного транспорта Лондона, чтобы описать JSON Web Tokens! Система обеспечивает простой, удобный и удобный способ авторизации поездки для миллионов людей, которые используют несколько видов транспорта — поезд, трамвай, автобус, паром (не показано на рисунке, чтобы избежать переполнения)!
Клиенты могут использовать все эти режимы только с одной проездной. Проездной билет приобретается или перезаряжается в торговом автомате. Карту всегда несет клиент, который просто нажимает на карточку у ворот въезда, чтобы воспользоваться поездкой.
JWT-авторизация для цифровых приложений работает аналогично. JWT походит на проездной. Приложения (поставщики услуг) похожи на разные виды транспорта. Торговый автомат, который продает путевые карточки, похож на эмитента токенов JWT.
Вам также может понравиться: токен JWT: легкая аутентификация на основе токена
Технический обзор авторизации на основе JWT
Как и в реальной аналогии, описанной выше, авторизация JWT включает три объекта и четыре шага, как описано на диаграмме последовательности ниже:
Объекты в потоке авторизации JWT
- Token Issuer — эмитент токена (система управления удостоверениями и доступом)
- Пользовательский интерфейс — браузер пользователя или мобильное приложение
- Поставщик услуг — веб-сайт или приложение, к которому пользователь хочет получить доступ
Шаги в авторизации JWT
Шаг 1. Эмитент токена предоставляет подписанный и зашифрованный токен пользовательскому интерфейсу
Пользователь аутентифицируется в Token Issuer, используя некоторый метод входа в систему, и просит Token Issuer предоставить токен. После успешной аутентификации эмитент токенов создает веб-токен JSON (JWT), имеющий следующую структуру:
Header.Payload. Подпись
Более подробную информацию о структуре JWT можно найти по адресу https://jwt.io/
Часть полезной нагрузки токена содержит пары ключ-значение, называемые утверждениями, которые предоставляют информацию о том, кем является пользователь и к чему ему разрешен доступ. Эмитент токенов создает токен с некоторыми утверждениями и отправляет токен пользователю. После того как токен создан и передан пользователю, ответственность за использование токена для получения доступа к сервису полностью ложится на пользователя. Пользователь должен передать токен поставщику услуг, чтобы получить доступ.
Поскольку ответственность за перенос токена возлагается на пользователя, необходимо решить следующие проблемы безопасности:
- Проблема № 1: Что делать, если конфиденциальные данные в токене прослушиваются перехватчиком?
- Проблема № 2: Что, если токен будет подделан для запроса услуг, предназначенных для других пользователей?
Хорошо известная криптография RSA с открытым ключом пригодится для решения этих проблем. Эмитент токенов применяет подпись и шифрование следующим образом:
- Токен-эмитент подписывает токен своим закрытым ключом и создает JWS (JWT — подписано).
- Затем Token Issuer шифрует JWS с помощью открытого ключа поставщика услуг. Зашифрованный JWS называется JWE (JWT — зашифрованный).
Только подписанный и зашифрованный токен (JWE) передается пользовательскому интерфейсу.
Пользователь может только нести JWE, но не может расшифровать его. Только поставщик услуг может расшифровать токен и просмотреть содержащиеся в нем утверждения. Поставщик услуг также обнаружит любое вмешательство в токен, так как токен подписан эмитентом токена.
JWE встроен в качестве заголовка авторизации в ответ HTTP, отправляемый клиенту.
Заголовок авторизации выглядит следующим образом:
HTTP
xxxxxxxxxx
1
Authorization: Bearer encrypted-json-web-token-text
Шаг 2. Пользовательский интерфейс отправляет токен вместе с запросом к поставщику услуг
Пользовательский интерфейс присоединяет JWE в качестве заголовка авторизации к HTTP-запросу, который он отправляет поставщику услуг.
Шаг 3. Поставщик услуг проверяет токен
Получив запрос от пользователя, поставщик услуг выполняет следующую последовательность проверок:
- У запроса есть токен?
- Можно ли расшифровать токен?
- Был ли подделан контент токена?
- Действителен ли токен?
- Каковы претензии внутри токена?
В конце проверки поставщик услуг извлекает полезную нагрузку из JWT и выясняет претензии.
Вот пример полезной нагрузки JWT, которую поставщик услуг извлекает из JWE.
JSON
xxxxxxxxxx
1
{
2
"iss": "token-provider-name",
3
"aud": "service-provider-name",
4
"iat": 1516227022,
5
"exp": 1516239022,
6
"jti": "unique-id-or-nonce",
7
"username": "John Doe",
8
"account": "123-456-789"
9
}
Претензии, найденные в вышеуказанной полезной нагрузке:
Запрос |
Описание |
ISS |
Эмитент токена (Token Issuer) |
ауд |
Аудитория (поставщик услуг, для которого предназначен токен) |
IAT |
Дата выдачи (время выдачи токена) |
ехр |
Expiry (Время истечения токена) |
JTI |
Идентификатор токена JWT (уникальный идентификатор или случайно сгенерированный одноразовый номер) |
имя пользователя |
Имя пользователя |
Счет |
Номер счета пользователя |
Шаг 4. Поставщик услуг отвечает на пользовательский интерфейс
Поставщик услуг дает соответствующий ответ пользовательскому интерфейсу на основе токена и утверждений.
Семинар: единый вход для Spring Boot Microservices с JWT
Теперь давайте используем веб-токены JSON для реализации единого входа в микросервисы Spring Boot. Полный исходный код этого семинара находится по адресу https://github.com/deargopinath/jwt-spring-boot.
Используемые технологии: Java , Spring Boot , JWT , Nimbus JOSE , JavaScript , CSS , HTML
Решение для единого входа состоит из двух частей:
- токен-эмитент — код для создания подписанного и зашифрованного JWT
- сервис-провайдер — код для расшифровки токена и авторизации пользователя с действительным токеном
Шаги для запуска кода
Шаг 1: Компиляция и запуск сервис-провайдера
Оболочка
xxxxxxxxxx
1
$ cd service-provider
2
$ mvn clean install
3
$ java -jar target/service-provider-1.0.0.jar
Шаг 2. Откройте поставщика услуг и убедитесь, что отображается сообщение об ошибке (неверный токен)
Шаг 3. Компиляция и запуск токена-эмитента в новом командном окне
Оболочка
xxxxxxxxxx
1
$ cd token-issuer
2
$ mvn clean install
3
$ java -jar target/token-issuer-1.0.0.jar
Шаг 4: Откройте Token Issuer и получите токен для доступа к поставщику услуг.
Заполните форму токена с соответствующей информацией (URL поставщика услуг, имя пользователя, номер учетной записи) и нажмите кнопку «Получить токен», чтобы получить подписанный и зашифрованный токен.
Шаг 5: Войдите в сервис-провайдер с помощью токена. Убедитесь, что поставщик услуг позволяет пользователю с действительным токеном
Нажатие на кнопку «Вход в систему с токеном» отправляет токен поставщику услуг.
Маркер будет встроен в «заголовок авторизации» HTTP-запроса.
Ответ от поставщика услуг появится в новой вкладке.
Поставщик услуг расшифровывает токен, проверяет подпись, а затем показывает страницу приветствия для пользователя с действительным токеном. Имя и номер учетной записи, указанные поставщиком услуг, извлекаются из зашифрованного токена.
Изучение Исходного кода
Теперь. Давайте изучим исходный код, чтобы увидеть, что происходит под капотом.
Токен Эмитент
Проект структурирован, как показано на рисунке ниже:
Давайте изучим код из этих двух файлов, чтобы понять функциональность приложения.
1. TokenService.java
Эта программа создает JSON Web Token, подписывает его с помощью закрытого ключа эмитента Token, а затем шифрует его с помощью открытого ключа поставщика услуг.
Подписывание с закрытым ключом эмитента защищает целостность данных.
Шифрование с открытым ключом поставщика услуг защищает конфиденциальность.
Джава
xxxxxxxxxx
1
public String getToken(RequestData requestData) {
2
String token = "unknown";
3
try {
4
String subject = requestData.getSubject();
5
String user = requestData.getUser();
6
String account = requestData.getAccount();
7
LOG.info("user = " + user + ", account = " + account + ", subject = " + subject);
8
RSAKey serverJWK = getJSONWebKey(serverPKCS);
9
// Set the token header
11
JWSHeader jwtHeader = new JWSHeader.Builder(JWSAlgorithm.RS256).jwk(serverJWK).build();
12
Calendar now = Calendar.getInstance();
13
Date issueTime = now.getTime();
14
now.add(Calendar.MINUTE, 10);
15
Date expiryTime = now.getTime();
16
String jti = String.valueOf(issueTime.getTime());
17
18
// Set the token payload
20
JWTClaimsSet jwtClaims = new JWTClaimsSet.Builder()
21
.issuer(issuer)
22
.subject(subject)
23
.issueTime(issueTime)
24
.expirationTime(expiryTime)
25
.claim("user", user)
26
.claim("account", account)
27
.jwtID(jti)
28
.build();
29
LOG.info("JWT claims = " + jwtClaims.toString());
30
// Sign the token with Issuer’s Private Key
33
SignedJWT jws = new SignedJWT(jwtHeader, jwtClaims);
34
RSASSASigner signer = new RSASSASigner(serverJWK);
35
jws.sign(signer);
36
JWEHeader jweHeader = new JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256,
37
EncryptionMethod.A256GCM).contentType("JWT").build();
38
JWEObject jwe = new JWEObject(jweHeader, new Payload(jws));
39
// Encrypt signed token with Service Provider’s public key
42
RSAKey clientPublicKey = getPublicKey(clientCertificate);
43
jwe.encrypt(new RSAEncrypter(clientPublicKey));
44
token = jwe.serialize();
45
LOG.info("Token = " + token);
46
} catch (final JOSEException e) {
47
LOG.error(e.toString());
48
}
49
return token;
50
}
51
}
2. TokenController.java
Эта программа отправляет подписанный и зашифрованный токен пользователю.
Токен внедряется как «Заголовок авторизации» HTTP-ответа, отправляемого пользователю. Затем пользователь выберет токен из заголовка авторизации и отправит его поставщику услуг.
Джава
1
"/api/jwe") (
2
public ResponseEntity<?> getToken( final RequestData requestData, final Errors errors) {
3
4
if (errors.hasErrors()) {
5
String errorMessage = errors
6
.getAllErrors()
7
.stream()
8
.map(x -> x.getDefaultMessage())
9
.collect(Collectors.joining(","));
10
LOG.error("Error = " + errorMessage);
11
return ResponseEntity.badRequest().body(errorMessage);
12
}
13
String subject = requestData.getSubject();
15
String jwe = tokenService.getToken(requestData);
16
String json = ("{\"subject\":\"" + subject
17
+ "\",\"token\":\"" + jwe + "\"}");
18
LOG.info("Token generated for " + subject);
19
final HttpHeaders headers = new HttpHeaders();
20
headers.add("Authorization", "Bearer " + jwe);
21
LOG.info("Authorization Header set with token");
22
return (new ResponseEntity<>(json, headers, HttpStatus.OK));
23
}
Поставщик услуг
Проект структурирован так, как показано на рисунке ниже:
Давайте изучим эти два файла, чтобы понять функциональность приложения.
1. WebsiteConfiguration.java
Эта программа настраивает управление доступом из разных источников, чтобы пользователи из домена токена-эмитента могли отправлять запросы в домен поставщика услуг.
Джава
xxxxxxxxxx
1
public class WebsiteConfiguration implements WebMvcConfigurer {
2
3
"${token.issuer.url}") (
4
private String tokenIssuer;
5
6
7
public void addCorsMappings(CorsRegistry registry) {
8
registry.addMapping("/**").allowedOrigins(tokenIssuer);
9
}
10
}
2. TokenValidator.java
Эта программа извлекает токен из заголовка авторизации, расшифровывает и проверяет его.
Джава
xxxxxxxxxx
1
public boolean isValid(HttpServletRequest request) {
2
3
Enumeration<String> headers = request.getHeaderNames();
4
// Extract the encrypted token (JWE) form the Authorization Header
6
while(headers.hasMoreElements()) {
7
String key = headers.nextElement();
8
if(key.trim().equalsIgnoreCase("Authorization")) {
9
String authorizationHeader = request.getHeader(key);
10
if(!authorizationHeader.isBlank()) {
11
String[] tokenData = authorizationHeader.split(" ");
12
if(tokenData.length == 2 &&
13
tokenData[0].trim().equalsIgnoreCase("Bearer")) {
14
token = tokenData[1];
15
LOG.info("Received token: " + token);
16
break;
17
}
18
}
19
}
20
}
21
try {
24
JWT jwt = JWTParser.parse(token);
25
26
// Decrypt JWE into Signed JWT (JWS)
27
if(jwt instanceof EncryptedJWT) {
28
EncryptedJWT jwe = (EncryptedJWT) jwt;
29
RSAKey clientJWK = getJSONWebKey(clientPKCS);
30
JWEDecrypter decrypter = new RSADecrypter(clientJWK);
31
jwe.decrypt(decrypter);
32
SignedJWT jws = jwe.getPayload().toSignedJWT();
33
34
// Verify the signature of JWS
36
RSAKey serverJWK = getPublicKey(serverCertificate);
37
RSASSAVerifier signVerifier = new RSASSAVerifier(serverJWK);
38
if(jws.verify(signVerifier)) {
39
// Extract the payload (claims) of JWT
40
JWTClaimsSet claims = jws.getJWTClaimsSet();
41
Date expiryTime = claims.getExpirationTime();
42
LOG.info("Expiry time = " + expiryTime.toString());
43
if(expiryTime.after(new Date())) {
44
user = claims.getStringClaim("user");
45
account = claims.getStringClaim("account");
46
LOG.info("Token validated for user = " + user
47
+ ", account = " + account);
48
return true;
49
}
50
}
51
}
52
} catch(ParseException | JOSEException ex) {
53
LOG.error(ex.toString());
54
}
55
return false;
56
}
Заключение
JSON Web Token (JWT) используется в современных интернет-решениях для аутентификации, таких как OpenID Connect и нескольких коммерческих инструментах управления идентификацией и доступом. В этой статье описывается простой, масштабируемый и безопасный метод авторизации микросервисов с помощью JSON Web Tokens.
Ссылки
- Веб-токен JSON (JWT)
- Nimbus JOSE для безопасных JWT
- Весенний ботинок
- IDE NetBeans для разработки Java Microservice сек
Дальнейшее чтение
Cookie против токенов: полное руководство
Четыре наиболее часто используемых метода аутентификации REST API