1. Обзор
В этом руководстве мы обсудим, как реализовать SSO — единый вход — с помощью Spring Security OAuth и Spring Boot, используя Keycloak в качестве сервера авторизации.
Мы будем использовать 4 отдельных приложения:
- Сервер авторизации — центральный механизм аутентификации.
- Сервер ресурсов — поставщик
Foo
s - Два клиентских приложения — приложения, использующие SSO
Проще говоря, когда пользователь пытается получить доступ к ресурсу через одно клиентское приложение, он сначала будет перенаправлен для аутентификации через сервер авторизации. Keycloak выполнит вход пользователя в систему, и, пока он все еще находится в первом приложении, если доступ ко второму клиентскому приложению осуществляется с помощью того же браузера, пользователю не нужно будет снова вводить свои учетные данные.
Мы собираемся использовать тип предоставления кода авторизации
из OAuth2 для делегирования аутентификации.
Мы будем использовать стек OAuth в Spring Security 5. Если вы хотите использовать устаревший стек Spring Security OAuth, ознакомьтесь с этой предыдущей статьей: Простой единый вход с помощью Spring Security OAuth2 (устаревший стек)
Согласно руководству по миграции :
Spring Security называет эту функцию входом OAuth 2.0, а Spring Security OAuth называет ее SSO.
Хорошо, давайте сразу.
2. Сервер авторизации
Ранее стек Spring Security OAuth предлагал возможность настроить сервер авторизации в качестве приложения Spring.
Однако стек OAuth устарел в Spring, и теперь мы будем использовать Keycloak в качестве нашего сервера авторизации.
Итак, на этот раз мы настроим наш сервер авторизации как встроенный сервер Keycloak в приложении Spring Boot .
В нашей предварительной конфигурации мы определим двух клиентов, ssoClient-1
и ssoClient-2 ,
по одному для каждого клиентского приложения.
3. Сервер ресурсов
Затем нам нужен сервер ресурсов или REST API, который предоставит нам Foo
s, которые будет потреблять наше клиентское приложение.
По сути, это то же самое, что мы использовали для наших клиентских приложений Angular ранее.
4. Клиентские приложения
Теперь давайте посмотрим на наше клиентское приложение Thymeleaf; мы, конечно же, будем использовать Spring Boot, чтобы минимизировать конфигурацию.
Имейте в виду, что нам потребуется 2 из них, чтобы продемонстрировать функциональность единого входа .
4.1. Зависимости Maven
Во-первых, нам понадобятся следующие зависимости в нашем pom.xml
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
</dependency>
<dependency>
<groupId>io.projectreactor.netty</groupId>
<artifactId>reactor-netty</artifactId>
</dependency>
Чтобы включить всю необходимую клиентскую поддержку, включая безопасность, нам просто нужно добавить spring-boot-starter-oauth2-client
. Кроме того, поскольку старый RestTemplate
будет объявлен устаревшим, мы будем использовать WebClient
, поэтому мы добавили spring-webflux
и Reactor -
Netty .
4.2. Конфигурация безопасности
Далее, самая важная часть, настройка безопасности нашего первого клиентского приложения:
@EnableWebSecurity
public class UiSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**")
.authorizeRequests()
.antMatchers("/")
.permitAll()
.anyRequest()
.authenticated()
.and()
.oauth2Login();
}
@Bean
WebClient webClient(ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2 =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrationRepository,
authorizedClientRepository);
oauth2.setDefaultOAuth2AuthorizedClient(true);
return WebClient.builder().apply(oauth2.oauth2Configuration()).build();
}
}
Основной частью этой конфигурации является метод oauth2Login()
, который используется для включения поддержки входа OAuth 2.0 в Spring Security. Поскольку мы используем Keycloak, который по умолчанию является решением единого входа для веб-приложений и веб-служб RESTful, нам не нужно добавлять какую-либо дополнительную конфигурацию для единого входа.
Наконец, мы также определили bean-компонент WebClient
, который действует как простой HTTP-клиент для обработки запросов, отправляемых на наш сервер ресурсов.
А вот и application.yml
:
spring:
security:
oauth2:
client:
registration:
custom:
client-id: ssoClient-1
client-secret: ssoClientSecret-1
scope: read,write
authorization-grant-type: authorization_code
redirect-uri: http://localhost:8082/ui-one/login/oauth2/code/custom
provider:
custom:
authorization-uri: http://localhost:8083/auth/realms/foreach/protocol/openid-connect/auth
token-uri: http://localhost:8083/auth/realms/foreach/protocol/openid-connect/token
user-info-uri: http://localhost:8083/auth/realms/foreach/protocol/openid-connect/userinfo
user-name-attribute: preferred_username
thymeleaf:
cache: false
server:
port: 8082
servlet:
context-path: /ui-one
resourceserver:
api:
project:
url: http://localhost:8081/sso-resource-server/api/foos/
Здесь spring.security.oauth2.client.registration
— это корневое пространство имен для регистрации клиента. Мы определили клиента с пользовательским
идентификатором регистрации . Затем мы определили его client-id
, client-secret
, scope
, author-grant-type
и redirect-uri
, которые, конечно же, должны быть такими же, как и для нашего сервера авторизации.
После этого мы определили нашего поставщика услуг или сервер авторизации, опять же с тем же идентификатором custom
, и перечислили его разные URI для использования Spring Security. Это все, что нам нужно определить, и фреймворк без проблем выполняет весь процесс входа в систему, включая перенаправление на Keycloak .
Также обратите внимание, что в нашем примере здесь мы развернули свой сервер авторизации, но, конечно, мы также можем использовать других сторонних поставщиков, таких как Facebook или GitHub .
4.3. Контроллер
Давайте теперь реализуем наш контроллер в клиентском приложении, чтобы запрашивать Foo
с нашего сервера ресурсов:
@Controller
public class FooClientController {
@Value("${resourceserver.api.url}")
private String fooApiUrl;
@Autowired
private WebClient webClient;
@GetMapping("/foos")
public String getFoos(Model model) {
List<FooModel> foos = this.webClient.get()
.uri(fooApiUrl)
.retrieve()
.bodyToMono(new ParameterizedTypeReference<List<FooModel>>() {
})
.block();
model.addAttribute("foos", foos);
return "foos";
}
}
Как мы видим, у нас есть только один метод, который выдает ресурсы шаблону foos .
Нам не нужно было добавлять какой-либо код для входа.
4.4. Внешний интерфейс
Теперь давайте взглянем на конфигурацию внешнего интерфейса нашего клиентского приложения. Мы не собираемся здесь заострять на этом внимание, главным образом потому, что мы уже рассмотрели это на сайте .
Наше клиентское приложение имеет очень простой интерфейс; вот index.html
:
<a class="navbar-brand" th:href="@{/foos/}">Spring OAuth Client Thymeleaf - 1</a>
<label>Welcome !</label> <br /> <a th:href="@{/foos/}">Login</a>
И foos.html
:
<a class="navbar-brand" th:href="@{/foos/}">Spring OAuth Client Thymeleaf -1</a>
Hi, <span sec:authentication="name">preferred_username</span>
<h1>All Foos:</h1>
<table>
<thead>
<tr>
<td>ID</td>
<td>Name</td>
</tr>
</thead>
<tbody>
<tr th:if="${foos.empty}">
<td colspan="4">No foos</td>
</tr>
<tr th:each="foo : ${foos}">
<td><span th:text="${foo.id}"> ID </span></td>
<td><span th:text="${foo.name}"> Name </span></td>
</tr>
</tbody>
</table>
Страница foos.html
требует аутентификации пользователей. Если пользователь, не прошедший проверку подлинности, попытается получить доступ к foos.html
, он сначала будет перенаправлен на страницу входа в Keycloak .
4.5. Второе клиентское приложение
Мы настроим второе приложение, Spring OAuth Client Thymeleaf -2
, используя другой client_id
ssoClient-2
.
В основном это будет то же самое, что и первое приложение, которое мы только что описали.
application.yml
будет отличаться тем, что будет включать в свой spring.security.oauth2.client.registration другой client_id
, client_secret
и redirect_uri
:
spring:
security:
oauth2:
client:
registration:
custom:
client-id: ssoClient-2
client-secret: ssoClientSecret-2
scope: read,write
authorization-grant-type: authorization_code
redirect-uri: http://localhost:8084/ui-two/login/oauth2/code/custom
И, конечно же, нам нужен другой порт сервера, чтобы мы могли запускать их параллельно:
server:
port: 8084
servlet:
context-path: /ui-two
Наконец, мы настроим интерфейсные HTML-коды, чтобы иметь заголовок Spring OAuth Client Thymeleaf — 2
вместо — 1
, чтобы мы могли различать их.
5. Тестирование поведения системы единого входа
Чтобы проверить поведение единого входа, давайте запустим наши приложения.
Для этого нам потребуются все наши 4 загрузочных приложения — сервер авторизации, сервер ресурсов и оба клиентских приложения.
Теперь давайте откроем браузер, скажем, Chrome, и войдем в Client-1
,
используя учетные данные john@test.com/123
. Затем в другом окне или вкладке нажмите URL-адрес Client-2
. Нажав кнопку входа, мы сразу же будем перенаправлены на страницу Foos
, минуя этап аутентификации.
Точно так же, если пользователь сначала входит в систему Client-2
, ему не нужно вводить свое имя пользователя/пароль для Client-1
.
6. Заключение
В этом руководстве мы сосредоточились на реализации единого входа с использованием Spring Security OAuth2 и Spring Boot с использованием Keycloak в качестве поставщика удостоверений.
Как всегда, полный исходный код можно найти на GitHub .