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

Сервер авторизации Spring Security OAuth

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

1. Введение

OAuth — это открытый стандарт, описывающий процесс авторизации. Его можно использовать для авторизации доступа пользователей к API. Например, REST API может ограничить доступ только для зарегистрированных пользователей с соответствующей ролью.

Сервер авторизации OAuth отвечает за аутентификацию пользователей и выдачу токенов доступа, содержащих пользовательские данные и соответствующие политики доступа.

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

В процессе мы создадим клиент-серверное приложение, которое будет получать список статей ForEach из REST API. Как для клиентских служб, так и для серверных служб потребуется аутентификация OAuth.

2. Реализация сервера авторизации

Начнем с настройки сервера авторизации OAuth. Он будет служить источником аутентификации как для ресурса статьи, так и для клиентских серверов.

2.1. Зависимости

Во-первых, нам нужно добавить несколько зависимостей в наш файл pom.xml :

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.5.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.5.4</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>0.2.0</version>
</dependency>

2.2. Конфигурация

Теперь давайте настроим порт, на котором будет работать наш сервер авторизации, установив свойство server.port в файле application.yml :

server:
port: 9000

После этого мы можем перейти к конфигурации bean-компонентов Spring. Во- первых, нам понадобится класс @Configuration , в котором мы создадим несколько bean-компонентов, специфичных для OAuth. Первый будет репозиторием клиентских сервисов. В нашем примере у нас будет один клиент, созданный с помощью класса построителя RegisteredClient :

@Configuration
@Import(OAuth2AuthorizationServerConfiguration.class)
public class AuthorizationServerConfig {
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("articles-client")
.clientSecret("{noop}secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri("http://127.0.0.1:8080/login/oauth2/code/articles-client-oidc")
.redirectUri("http://127.0.0.1:8080/authorized")
.scope(OidcScopes.OPENID)
.scope("articles.read")
.build();
return new InMemoryRegisteredClientRepository(registeredClient);
}
}

Мы настраиваем следующие свойства:

  • Идентификатор клиента — Spring будет использовать его для определения того, какой клиент пытается получить доступ к ресурсу.
  • Секретный код клиента — секрет, известный клиенту и серверу, который обеспечивает доверие между ними.
  • Метод аутентификации — в нашем случае мы будем использовать обычную аутентификацию, которая представляет собой просто имя пользователя и пароль.
  • Тип гранта авторизации — мы хотим, чтобы клиент мог генерировать как код авторизации, так и токен обновления.
  • URI перенаправления — клиент будет использовать его в потоке на основе перенаправления.
  • Scope — этот параметр определяет полномочия, которые может иметь клиент. В нашем случае у нас будет обязательный OidcScopes.OPENID и наш пользовательский – статьи. читать

Затем давайте настроим bean-компонент для применения безопасности OAuth по умолчанию и создания страницы входа в форму по умолчанию:

@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
return http.formLogin(Customizer.withDefaults()).build();
}

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

@Bean
public JWKSource<SecurityContext> jwkSource() {
RSAKey rsaKey = generateRsa();
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}

private static RSAKey generateRsa() {
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() {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
return keyPairGenerator.generateKeyPair();
}

За исключением ключа подписи, каждый сервер авторизации также должен иметь уникальный URL-адрес издателя. Мы настроим его как псевдоним localhost для http://auth-server на порту 9000 , создав bean- компонент ProviderSettings :

@Bean
public ProviderSettings providerSettings() {
return ProviderSettings.builder()
.issuer("http://auth-server:9000")
.build();
}

Кроме того, мы добавим запись « 127.0.0.1 auth-server » в наш файл /etc/hosts . Это позволяет нам запускать клиент и сервер авторизации на нашем локальном компьютере и позволяет избежать проблем с перезаписью файлов cookie сеанса между ними.

Наконец, мы включим модуль веб-безопасности Spring с аннотированным классом конфигурации @EnableWebSecurity :

@EnableWebSecurity
public class DefaultSecurityConfig {

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

// ...
}

Здесь мы вызываем authorizeRequests.anyRequest().authenticated() , чтобы требовать аутентификацию для всех запросов, и мы обеспечиваем аутентификацию на основе формы, вызывая метод formLogin(defaults()) .

Кроме того, мы определим набор пользователей-примеров, которых будем использовать для тестирования. Для этого примера давайте создадим репозиторий только с одним пользователем-администратором:

@Bean
UserDetailsService users() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("admin")
.password("password")
.build();
return new InMemoryUserDetailsManager(user);
}

3. Сервер ресурсов

Теперь мы создадим сервер ресурсов, который будет возвращать список статей из конечной точки GET. Конечные точки должны разрешать только запросы, аутентифицированные на нашем сервере OAuth.

3.1. Зависимости

Во-первых, давайте включим необходимые зависимости:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.5.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.5.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
<version>2.5.4</version>
</dependency>

3.2. Конфигурация

Прежде чем мы начнем с кода реализации, мы должны настроить некоторые свойства в файле application.yml . Первый порт сервера:

server:
port: 8090

Далее пришло время для настройки безопасности. Нам нужно настроить правильный URL-адрес для нашего сервера аутентификации с хостом и портом, которые мы настроили в bean- компоненте ProviderSettings ранее:

spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://auth-server:9000

Теперь мы можем настроить нашу конфигурацию веб-безопасности. Опять же, мы хотим прямо сказать, что каждый запрос к ресурсам статей должен быть авторизован и иметь соответствующие права на article.read :

@EnableWebSecurity
public class ResourceServerConfig {

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.mvcMatcher("/articles/**")
.authorizeRequests()
.mvcMatchers("/articles/**")
.access("hasAuthority('SCOPE_articles.read')")
.and()
.oauth2ResourceServer()
.jwt();
return http.build();
}
}

Как показано здесь, мы также вызываем метод oauth2ResourceServer() , который настроит подключение к серверу OAuth на основе конфигурации application.yml .

3.3. Статьи Контроллер

Наконец, мы создадим контроллер REST, который будет возвращать список статей в конечной точке GET /articles :

@RestController
public class ArticlesController {

@GetMapping("/articles")
public String[] getArticles() {
return new String[] { "Article 1", "Article 2", "Article 3" };
}
}

4. API-клиент

В последней части мы создадим клиент REST API, который будет получать список статей с сервера ресурсов.

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

Для начала давайте включим необходимые зависимости:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.5.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.5.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
<version>2.5.4</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
<version>5.3.9</version>
</dependency>
<dependency>
<groupId>io.projectreactor.netty</groupId>
<artifactId>reactor-netty</artifactId>
<version>1.0.9</version>
</dependency>

4.2. Конфигурация

Как и ранее, мы определим некоторые свойства конфигурации для аутентификации:

server:
port: 8080

spring:
security:
oauth2:
client:
registration:
articles-client-oidc:
provider: spring
client-id: articles-client
client-secret: secret
authorization-grant-type: authorization_code
redirect-uri: "http://127.0.0.1:8080/login/oauth2/code/{registrationId}"
scope: openid
client-name: articles-client-oidc
articles-client-authorization-code:
provider: spring
client-id: articles-client
client-secret: secret
authorization-grant-type: authorization_code
redirect-uri: "http://127.0.0.1:8080/authorized"
scope: articles.read
client-name: articles-client-authorization-code
provider:
spring:
issuer-uri: http://auth-server:9000

Теперь давайте создадим экземпляр WebClient для выполнения HTTP-запросов к нашему ресурсному серверу. Мы будем использовать стандартную реализацию с одним добавлением фильтра авторизации OAuth:

@Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
return WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.build();
}

WebClient требует OAuth2AuthorizedClientManager в качестве зависимости. Давайте создадим реализацию по умолчанию:

@Bean
OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {

OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

return authorizedClientManager;
}

Наконец, мы настроим веб-безопасность:

@EnableWebSecurity
public class SecurityConfig {

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests ->
authorizeRequests.anyRequest().authenticated()
)
.oauth2Login(oauth2Login ->
oauth2Login.loginPage("/oauth2/authorization/articles-client-oidc"))
.oauth2Client(withDefaults());
return http.build();
}
}

Здесь, как и на других серверах, нам потребуется аутентификация каждого запроса. Кроме того, нам нужно настроить URL-адрес страницы входа (определенный в конфигурации .yml ) и клиент OAuth.

4.3. Статьи Клиентский контроллер

Наконец, мы можем создать контроллер доступа к данным. Мы будем использовать ранее настроенный WebClient для отправки HTTP-запроса на наш сервер ресурсов:

@RestController
public class ArticlesController {

private WebClient webClient;

@GetMapping(value = "/articles")
public String[] getArticles(
@RegisteredOAuth2AuthorizedClient("articles-client-authorization-code") OAuth2AuthorizedClient authorizedClient
) {
return this.webClient
.get()
.uri("http://127.0.0.1:8090/articles")
.attributes(oauth2AuthorizedClient(authorizedClient))
.retrieve()
.bodyToMono(String[].class)
.block();
}
}

В приведенном выше примере мы берем токен авторизации OAuth из запроса в форме класса OAuth2AuthorizedClient . Spring автоматически связывает его с помощью аннотации @RegisterdOAuth2AuthorizedClient с правильной идентификацией. В нашем случае он извлекается из кода article-client-authorizaiton , который мы ранее настроили в файле .yml .

Этот токен авторизации далее передается в HTTP-запрос.

4.4. Доступ к списку статей

Теперь, когда мы заходим в браузер и пытаемся получить доступ к странице http://127.0.0.1:8080/articles , мы автоматически перенаправляемся на страницу входа на сервер OAuth по адресу http://auth-server:9000/login. URL-адрес:

./f91124ff8aeea828d4edecebea89056a.png

После предоставления правильного имени пользователя и пароля сервер авторизации перенаправит нас обратно на запрошенный URL — список статей.

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

5. Вывод

В этой статье мы узнали, как установить, настроить и использовать сервер авторизации Spring Security OAuth.

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