Перейти к основному содержимому

Сервер ресурсов OAuth 2.0 с Spring Security 5

· 8 мин. чтения

Задача: Сумма двух чисел

Напишите функцию twoSum. Которая получает массив целых чисел nums и целую сумму target, а возвращает индексы двух чисел, сумма которых равна target. Любой набор входных данных имеет ровно одно решение, и вы не можете использовать один и тот же элемент дважды. Ответ можно возвращать в любом порядке...

ANDROMEDA

1. Обзор

В этом руководстве мы узнаем , как настроить сервер ресурсов OAuth 2.0 с помощью Spring Security 5 .

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

Перед тем, как перейти к примерам реализации и кода, мы установим некоторую предысторию.

2. Немного предыстории

2.1. Что такое JWT и непрозрачные токены?

JWT или JSON Web Token — это способ безопасной передачи конфиденциальной информации в широко распространенном формате JSON. Содержащаяся информация может быть о пользователе или о самом токене, например, о сроке его действия и эмитенте.

С другой стороны, непрозрачный токен, как следует из названия, непрозрачен с точки зрения информации, которую он несет. Токен — это просто идентификатор, указывающий на информацию, хранящуюся на сервере авторизации, — он проверяется путем самоанализа на стороне сервера.

2.2. Что такое сервер ресурсов?

В контексте OAuth 2.0 сервер ресурсов — это приложение, которое защищает ресурсы с помощью токенов OAuth . Эти токены выдаются сервером авторизации, как правило, клиентскому приложению. Задача сервера ресурсов заключается в проверке маркера перед предоставлением ресурса клиенту.

Срок действия токена определяется несколькими вещами:

  • Этот токен пришел с настроенного сервера авторизации?
  • Срок действия не истек?
  • Является ли этот ресурсный сервер целевой аудиторией?
  • Обладает ли токен необходимыми полномочиями для доступа к запрошенному ресурсу?

Для наглядности давайте посмотрим на диаграмму последовательности потока кода авторизации и посмотрим на всех участников в действии:

./452cf47076af2122db20aa1c0ea5d51b.png

Как мы видим на шаге 8, когда клиентское приложение вызывает API сервера ресурсов для доступа к защищенному ресурсу, оно сначала обращается к серверу авторизации для проверки токена, содержащегося в заголовке Authorization: Bearer запроса, а затем отвечает клиенту.

Шаг 9 — это то, на чем мы сосредоточимся в этом уроке.

Хорошо, теперь давайте перейдем к части кода. Мы настроим сервер авторизации с помощью Keycloak, сервер ресурсов, проверяющий токены JWT, еще один сервер ресурсов, проверяющий непрозрачные токены, и пару тестов JUnit для имитации клиентских приложений и проверки ответов.

3. Сервер авторизации

Во-первых, мы настроим сервер авторизации или то, что выдает токены.

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

Наш встроенный сервер Keycloak имеет два определенных клиента — fooClient и barClient — соответствующих нашим двум приложениям сервера ресурсов.

4. Сервер ресурсов — использование JWT

Наш сервер ресурсов будет состоять из четырех основных компонентов:

  • Модель – ресурс для защиты
  • API — контроллер REST для предоставления ресурса
  • Конфигурация безопасности — класс для определения контроля доступа к защищенному ресурсу, предоставляемому API.
  • application.yml — файл конфигурации для объявления свойств, включая информацию о сервере авторизации

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

4.1. Зависимости Maven

В основном нам понадобится spring-boot-starter-oauth2-resource-server , стартер Spring Boot для поддержки сервера ресурсов. Этот стартер включает Spring Security по умолчанию, поэтому нам не нужно добавлять его явно:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>

Кроме того, мы также добавили веб-поддержку.

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

4.2. Модель

Для простоты мы будем использовать Foo , POJO, в качестве нашего защищенного ресурса:

public class Foo {
private long id;
private String name;

// constructor, getters and setters
}

4.3. API

Вот наш контроллер rest, чтобы сделать Foo доступным для манипуляций:

@RestController
@RequestMapping(value = "/foos")
public class FooController {

@GetMapping(value = "/{id}")
public Foo findOne(@PathVariable Long id) {
return new Foo(Long.parseLong(randomNumeric(2)), randomAlphabetic(4));
}

@GetMapping
public List findAll() {
List fooList = new ArrayList();
fooList.add(new Foo(Long.parseLong(randomNumeric(2)), randomAlphabetic(4)));
fooList.add(new Foo(Long.parseLong(randomNumeric(2)), randomAlphabetic(4)));
fooList.add(new Foo(Long.parseLong(randomNumeric(2)), randomAlphabetic(4)));
return fooList;
}

@ResponseStatus(HttpStatus.CREATED)
@PostMapping
public void create(@RequestBody Foo newFoo) {
logger.info("Foo created");
}
}

Как видно, у нас есть возможность ПОЛУЧИТЬ все Foo , ПОЛУЧИТЬ Foo по идентификатору и POST Foo .

4.4. Конфигурация безопасности

В этом классе конфигурации мы определяем уровни доступа для нашего ресурса:

@Configuration
public class JWTSecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authz -> authz
.antMatchers(HttpMethod.GET, "/foos/**").hasAuthority("SCOPE_read")
.antMatchers(HttpMethod.POST, "/foos").hasAuthority("SCOPE_write")
.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2.jwt());
}
}

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

Кроме того, мы добавили вызов jwt() с использованием DSL oauth2ResourceServer() для указания типа токенов, поддерживаемых нашим сервером здесь .

4.5. приложение.yml

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

server: 
port: 8081
servlet:
context-path: /resource-server-jwt

spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://localhost:8083/auth/realms/foreach

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

Чтобы эта проверка работала с использованием свойства issuer-uri , сервер авторизации должен быть запущен и работать. В противном случае сервер ресурсов не запустится.

Если нам нужно запустить его независимо, мы можем вместо этого указать свойство jwk-set-uri , чтобы указать на конечную точку сервера авторизации, предоставляющую открытые ключи:

jwk-set-uri: http://localhost:8083/auth/realms/foreach/protocol/openid-connect/certs

И это все, что нам нужно, чтобы наш сервер проверял токены JWT.

4.6. Тестирование

Для тестирования мы настроим JUnit. Чтобы выполнить этот тест, нам нужен сервер авторизации, а также сервер ресурсов.

Давайте проверим, что мы можем получить Foo s от resource-server-jw t с токеном области чтения в нашем тесте:

@Test
public void givenUserWithReadScope_whenGetFooResource_thenSuccess() {
String accessToken = obtainAccessToken("read");

Response response = RestAssured.given()
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
.get("http://localhost:8081/resource-server-jwt/foos");
assertThat(response.as(List.class)).hasSizeGreaterThan(0);
}

В приведенном выше коде в строке № 3 мы получаем токен доступа с областью чтения с сервера авторизации, охватывающий шаги с 1 по 7 нашей диаграммы последовательности.

Шаг 8 выполняется вызовом RestAssured get () . Шаг 9 выполняется сервером ресурсов с конфигурациями, которые мы видели, и он прозрачен для нас как пользователей.

5. Сервер ресурсов — использование непрозрачных токенов

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

5.1. Зависимости Maven

Для поддержки непрозрачных токенов нам дополнительно понадобится зависимость oauth2-oidc-sdk :

<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>oauth2-oidc-sdk</artifactId>
<version>8.19</version>
<scope>runtime</scope>
</dependency>

5.2. Модель и контроллер

Для этого мы добавим ресурс Bar :

public class Bar {
private long id;
private String name;

// constructor, getters and setters
}

У нас также будет BarController с конечными точками, похожими на наш FooController раньше, чтобы распределять Bar s.

5.3. приложение.yml

В application.yml здесь нам нужно добавить самоанализ-uri , соответствующий конечной точке самоанализа нашего сервера авторизации. Как упоминалось ранее, непрозрачный токен проверяется следующим образом: ``

server: 
port: 8082
servlet:
context-path: /resource-server-opaque

spring:
security:
oauth2:
resourceserver:
opaque:
introspection-uri: http://localhost:8083/auth/realms/foreach/protocol/openid-connect/token/introspect
introspection-client-id: barClient
introspection-client-secret: barClientSecret

5.4. Конфигурация безопасности

Сохраняя уровни доступа аналогичными Foo для ресурса Bar , этот класс конфигурации также вызывает opaqueToken() с использованием DSL oauth2ResourceServer() для указания использования непрозрачного типа токена :

@Configuration
public class OpaqueSecurityConfig extends WebSecurityConfigurerAdapter {

@Value("${spring.security.oauth2.resourceserver.opaque.introspection-uri}")
String introspectionUri;

@Value("${spring.security.oauth2.resourceserver.opaque.introspection-client-id}")
String clientId;

@Value("${spring.security.oauth2.resourceserver.opaque.introspection-client-secret}")
String clientSecret;

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authz -> authz
.antMatchers(HttpMethod.GET, "/bars/**").hasAuthority("SCOPE_read")
.antMatchers(HttpMethod.POST, "/bars").hasAuthority("SCOPE_write")
.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2
.opaqueToken(token -> token.introspectionUri(this.introspectionUri)
.introspectionClientCredentials(this.clientId, this.clientSecret)));
}
}

Здесь мы также указываем учетные данные клиента, соответствующие клиенту сервера авторизации, который мы будем использовать. Мы определили их ранее в нашем application.yml .

5.5. Тестирование

Мы настроим JUnit для нашего непрозрачного сервера ресурсов на основе токенов, аналогично тому, как мы сделали это для JWT.

В этом случае давайте проверим, может ли токен доступа с областью записи отправить POST Bar в resource-server-opaque :

@Test
public void givenUserWithWriteScope_whenPostNewBarResource_thenCreated() {
String accessToken = obtainAccessToken("read write");
Bar newBar = new Bar(Long.parseLong(randomNumeric(2)), randomAlphabetic(4));

Response response = RestAssured.given()
.contentType(ContentType.JSON)
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
.body(newBar)
.log()
.all()
.post("http://localhost:8082/resource-server-opaque/bars");
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED.value());
}

Если мы получим статус CREATED обратно, это означает, что сервер ресурсов успешно проверил непрозрачный токен и создал для нас панель .

6. Заключение

В этом руководстве мы увидели, как настроить приложение сервера ресурсов на основе Spring Security для проверки JWT, а также непрозрачных токенов.

Как мы видели, с минимальной настройкой Spring позволяет беспрепятственно проверять токены с эмитентом и отправлять ресурсы запрашивающей стороне — в нашем случае это тест JUnit.

Как всегда, исходный код доступен на GitHub .