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-адрес:
После предоставления правильного имени пользователя и пароля сервер авторизации перенаправит нас обратно на запрошенный URL — список статей.
Дальнейшие запросы к конечной точке статей не потребуют входа в систему, так как токен доступа будет храниться в файле cookie.
5. Вывод
В этой статье мы узнали, как установить, настроить и использовать сервер авторизации Spring Security OAuth.
Как всегда, весь исходный код доступен на GitHub .