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 . Эти токены выдаются сервером авторизации, как правило, клиентскому приложению. Задача сервера ресурсов заключается в проверке маркера перед предоставлением ресурса клиенту.
Срок действия токена определяется несколькими вещами:
- Этот токен пришел с настроенного сервера авторизации?
- Срок действия не истек?
- Является ли этот ресурсный сервер целевой аудиторией?
- Обладает ли токен необходимыми полномочиями для доступа к запрошенному ресурсу?
Для наглядности давайте посмотрим на диаграмму последовательности потока кода авторизации и посмотрим на всех участников в действии:
Как мы видим на шаге 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 .