Реализация сервера авторизации OAuth с помощью сервера авторизации Spring

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Сервер авторизации в OAuth предназначен для выдачи маркера доступа, который позволяет клиентскому приложению использовать этот маркер доступа для запроса ресурса, который ему нужно получить. Сервер ресурсов будет подтверждать этот маркер доступа с помощью сервера авторизации каждый раз, когда клиентское приложение запрашивает ресурс, чтобы определить, следует ли разрешить клиентскому приложению доступ к этому ресурсу. Вы можете использовать множество различных открытых источников, таких как Keycloak, Spring Security OAuth (устаревший), или же новый проект Spring под названием Spring Authorization Server для реализации этого сервера авторизации. В этом руководстве я покажу вам, как использовать сервер авторизации Spring (Spring Authorization Server) для реализации сервера авторизации OAuth (OAuth Authorization Server)!

Сначала я создам новый проект Spring Boot с Web Starter, Security Starter:

и сервер авторизации Spring:

<dependency>
    <groupId>org.springframework.security.experimental</groupId>
    <artifactId>spring-security-oauth2-authorization-server</artifactId>
    <version>0.1.2</version>
</dependency>

для примера.

Результат:

Конфигурация сервера авторизации

Сначала я создам новый класс AuthorizationServerConfiguration для настройки сервера авторизации.

По умолчанию сервер авторизации Spring поддерживает класс OAuth2AuthorizationServerConfiguration с конфигурациями по умолчанию для сервера авторизации. Если взглянуть на код этого класса, то можно увидеть, что он определяет метод applyDefaultSecurity(), который инициализирует объект OAuth2AuthorizationServerConfigurer с целью применения конфигураций по умолчанию, которые определяет этот класс OAuth2AuthorizationServerConfigurer:

public static void applyDefaultSecurity(HttpSecurity http) throws Exception {
    OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
            new OAuth2AuthorizationServerConfigurer<>();
    RequestMatcher endpointsMatcher = authorizationServerConfigurer
            .getEndpointsMatcher();

    http
        .requestMatcher(endpointsMatcher)
        .authorizeRequests(authorizeRequests ->
            authorizeRequests.anyRequest().authenticated()
        )
        .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
        .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
        .apply(authorizationServerConfigurer);
}

Как вы можете видеть, метод applyDefaultSecurity() также определяет безопасность для конечных точек сервера авторизации по умолчанию.

Класс OAuth2AuthorizationServerConfiguration также определяет бин (bean) для класса SecurityFilterChain, который вызывает метод applyDefaultSecurity() для регистрации этих конфигураций по умолчанию в Spring Security сервера авторизации.

Вы можете импортировать этот класс OAuth2AuthorizationServerConfiguration с помощью аннотации Spring @Import, чтобы использовать эти конфигурации по умолчанию:

package com.huongdanjava.springauthorizationserver;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;

@Configuration
@Import(OAuth2AuthorizationServerConfiguration.class)
public class AuthorizationServerConfiguration {

}

или если вы хотите добавить что-то из пользовательского кода, то давайте объявим бин для класса SecurityFilterChain и вызовем метод applyDefaultSecurity() следующим образом:

package com.huongdanjava.springauthorizationserver;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class AuthorizationServerConfiguration {

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);

        return http.formLogin(Customizer.withDefaults()).build();
    }

}

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

При использовании сервера авторизации нам важно определить веб-ключ JSON для проверки информации в маркере доступа, который пользователь запросил у сервера ресурсов, выданном сервером авторизации. Для завершения настройки этого сервера авторизации требуется бин JwtDecoder с объектом класса JWKSource. Мы можем определить бины для этих объектов следующим образом:

@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
    return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}

@Bean
public JWKSource<SecurityContext> jwkSource() throws NoSuchAlgorithmException {
    RSAKey rsaKey = generateRsa();
    JWKSet jwkSet = new JWKSet(rsaKey);

    return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}

private static RSAKey generateRsa() throws NoSuchAlgorithmException {
    KeyPair keyPair = generateRsaKey();
    RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
    RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();

    return new RSAKey.Builder(publicKey)
        .privateKey(privateKey)
        .keyID(UUID.randomUUID().toString())
        .build();
}

private static KeyPair generateRsaKey() throws NoSuchAlgorithmException {
    KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
    keyPairGenerator.initialize(2048);

    return keyPairGenerator.generateKeyPair();
}

Конфигурация Spring Security

Когда сервер авторизации осуществляет перенаправление на страницу входа в систему, поскольку пользователь не прошел аутентификацию, нам следует определить еще один SecurityFilterChain для обработки этого запроса и всех остальных запросов сервера авторизации. Это необходимо, поскольку класс OAuth2AuthorizationServerConfiguration определяет безопасность только для конечных точек сервера авторизации по умолчанию.

Мы можем определить SecurityFilterChain следующим образом:

package com.huongdanjava.springauthorizationserver;

import org.springframework.context.annotation.Bean;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@EnableWebSecurity
public class SpringSecurityConfiguration {

    @Bean
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeRequests(authorizeRequests ->
                authorizeRequests.anyRequest().authenticated()
            )
            .formLogin(Customizer.withDefaults());

        return http.build();
    }

}

На этом этапе страница входа будет отображаться, если пользователь не вошел в систему.

Регистрация клиента на сервере авторизации

Сервер авторизации Spring использует класс RegisteredClient для объявления информации клиента, зарегистрированного на сервере авторизации, и реализует интерфейс RegisteredClientRepository для хранения информации всех этих клиентов.

Мы можем объявить информацию о клиенте, используя память или соответствующую базу данных:

Для простоты я буду использовать память следующим образом:

@Bean
public RegisteredClientRepository registeredClientRepository() {
    RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
        .clientId("huongdanjava")
        .clientSecret("{noop}123456")
        .clientAuthenticationMethod(ClientAuthenticationMethod.POST)
        .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
        .redirectUri("https://oidcdebugger.com/debug")
        .scope(OidcScopes.OPENID)
        .build();

    return new InMemoryRegisteredClientRepository(registeredClient);
}

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

Что такое идентификатор клиента, объяснять не нужно! По поводу типа гранта авторизации, следует отметить, что сервер авторизации Spring поддерживает все типы грантов OAuth 2.

Секрет клиента зависит от типа клиента, который мы хотим определить, если наш клиент конфиденциальный, см. также Типы клиентов в OAuth 2.0, то секрет клиента обязателен. Здесь вам нужно объявить, как зашифровать клиентский секрет с помощью PasswordEncoder, если вы не хотите шифровать его в целях тестирования, то можно использовать NoOpPasswordEncoder, объявив "{noop}" в начале клиентского секрета, как я сделал выше. Помните, что это применяется только в тестовых целях!

Если клиент является конфиденциальным, нам также понадобится Метод Client Authentication для определения маркера доступа.

В зависимости от типа гранта клиента, который вы определяете, есть и другая необходимая информация, которую нам нужно объявить. Например, в моем случае я определяю клиента с типом разрешения authorization_code, поэтому мне нужно определить redirect_uri. Здесь я буду использовать инструмент https://oidcdebugger.com/ для получения кода авторизации, поэтому я определяю redirect_uri со значением https://oidcdebugger.com/debug, как это можно видеть в примере.

В зависимости от ваших потребностей, давайте определим информацию о клиенте соответствующим образом.

Регистрация пользователя на сервере авторизации

Для регистрации пользователя на сервере авторизации используется память со следующим объявлением:

@Bean
public UserDetailsService users() {
    UserDetails user = User.withDefaultPasswordEncoder()
        .username("admin")
        .password("password")
        .roles("ADMIN")
        .build();

    return new InMemoryUserDetailsManager(user);
}

Итак, на данном этапе мы завершили базовую конфигурацию для Authorization Server.

Чтобы проверить результаты, я воспользуюсь инструментом https://oidcdebugger.com/, как упоминалось выше, со следующим объявлением:

Нажмите кнопку Send request (Отправить запрос) на этой странице, после чего вы откроете страницу входа на сервер авторизации, которая будет выглядеть следующим образом:

Войдите в систему с информацией, которую мы объявили выше, и увидите следующие результаты:

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


Материал подготовлен в рамках курса «Разработчик на Spring Framework». Если вам интересно узнать подробнее о формате обучения и программе, познакомиться с преподавателем курса — приглашаем на день открытых дверей онлайн. Регистрация здесь.

Источник: https://habr.com/ru/company/otus/blog/570130/


Интересные статьи

Интересные статьи

Мне всегда нравились карты городов, и несколько недель назад я решил создать свою собственную, художественную версию. Немного погуглив, я обнаружил крутое руководство, написанное Фрэнком ...
Слева — Танай Тандон, в 17 лет в 2014 году основал стартап Athelas для диагностики малярии при помощи смартфона. Справа — соосновательница Athelas Дипика Бодопати. Несколько лет н...
Привет, Хабр. Данный текст можно считать продолжением статьи "Разбираем звук Dial-up модема", в которой разбирался метод установки связи между модемами. Сегодня мы пойдем дальше, и п...
Кустикова Валентина, Васильев Евгений, Вихрев Иван, Дудченко Антон, Уткин Константин и Коробейников Алексей. Intel Distribution of OpenVINO Toolkit — набор библиотек для разработки приложений,...
Взгляните на экран. Что вы видите? Страницу веб-сайта с текстом и картинками, верно? Но, а если копнуть глубже? Все эти разные по смысловой нагрузке и способу подачи элементы состоят из цифро...