1. 概述
在本教程中,我們將討論如何使用 Spring Security OAuth 和 Spring Boot,藉助 Keycloak 作為授權服務器,實現 單點登錄 (SSO) 的實施方法。我們將使用 4 個獨立應用程序:
- 授權服務器,它是中心身份驗證機制。
- 資源服務器,它是 Foo 提供者。
- 客户端應用程序,這些應用程序使用 SSO。
我們將使用 OAuth2 中的 授權碼 grant 類型來驅動身份驗證的委託。
我們將使用 Spring Security 5 中的 OAuth 棧。 如果您想使用 Spring Security OAuth 的遺留棧,請查看這篇文章:Simple Single Sign-On with Spring Security OAuth2 (legacy stack)
根據 遷移指南Spring Security 將此功能稱為 OAuth 2.0 登錄,而 Spring Security OAuth 則將其稱為 SSO
好了,讓我們直接開始吧。2. 授權服務器
此前,Spring Security OAuth 堆棧提供了將授權服務器設置為 Spring Application 的可能性。然而,Spring 已經廢棄了 OAuth 堆棧,現在我們將使用 Keycloak 作為我們的授權服務器。
因此,我們現在將授權服務器設置為 Spring Boot 應用中的嵌入式 Keycloak 服務器。
在我們的預配置中,我們將定義兩個客户端,ssoClient-1 和 ssoClient-2,一個用於每個客户端應用程序。
3. 資源服務器
接下來,我們需要一個資源服務器,或者説是 REST API,它將為我們的客户端應用程序提供 Foo。
它與我們之前為 Angular 客户端應用程序所使用的基本相同。
4. The Client Applications
Now let’s look at our Thymeleaf Client Application; we’ll, of course, use Spring Boot to minimize the configuration.
Do keep in mind that we’ll need to have 2 of these to demonstrate Single Sign-On functionality.
4.1. Maven Dependencies
First, we will need the following dependencies in our 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>
To include all the client support we’ll require, including security, we just need to add spring-boot-starter-oauth2-client.
Also, since the old RestTemplate is going to be deprecated, we’re going to use WebClient, and that’s why we added spring-webflux and reactor-netty.
4.2. Security Configuration
Next, the most important part, the security configuration of our first client application:
@EnableWebSecurity
public class UiSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/", "/login**")
.permitAll()
.anyRequest()
.authenticated()
.and()
.oauth2Login();
return http.build();
}
@Bean
WebClient webClient(ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2 =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrationRepository,
authorizedClientRepository);
oauth2.setDefaultOAuth2AuthorizedClient(true);
return WebClient.builder()
.apply(oauth2.oauth2Configuration())
.build();
}
}
The core part of this configuration is the oauth2Login() method, which is used to enable Spring Security’s OAuth 2.0 Login support.
Since we’re using Keycloak, which is by default a single sign-on solution for web apps and RESTful web services, we do not need to add any further configuration for SSO.
Finally, we also defined a WebClient bean to act as a simple HTTP Client to handle requests to be sent to our Resource Server.
And here’s the application.yml:
spring:
security:
oauth2:
client:
registration:
custom:
client-id: ssoClient-1
client-secret: ssoClientSecret-1
scope: read,write,openid
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/baeldung/protocol/openid-connect/auth
token-uri: http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/token
user-info-uri: http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/userinfo
jwk-set-uri: http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/certs
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/
Here, spring.security.oauth2.client.registration is the root namespace for registering a client. We defined a client with registration id custom. Then we defined its client-id, client-secret, scope, authorization-grant-type and redirect-uri, which of course, should be the same as that defined for our Authorization Server.
After that, we defined our service provider or the Authorization Server, again with the same id of custom, and listed down its different URI’s for Spring Security to use. That’s all we need to define, and the framework does the entire logging-in process, including redirection to Keycloak, seamlessly for us.
Also note that, in our example here, we rolled out our Authorization Server, but of course we can also use other, third-party providers such as Facebook or GitHub.
4.3. The Controller
Let’s now implement our controller in the Client App to ask for Foos from our Resource Server:
@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";
}
}
As we can see, we have only one method here that’ll dish out the resources to the foos template. We did not have to add any code for login.
4.4. Front End
Now, let’s take a look at the front-end configuration of our client application. We’re not going to focus on that here, mainly because we already covered in on the site.
Our client application here has a very simple front-end; here’s the 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>
And the 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>
The foos.html page needs the users to be authenticated. If a non-authenticated user tries to access foos.html, they’ll be redirected to Keycloak’s login page first.
4.5. The Second Client Application
We’ll configure a second application, Spring OAuth Client Thymeleaf -2 using another client_id ssoClient-2.
It’ll mostly be the same as the first application we just described.
The application.yml will differ to include a different client_id, client_secret and redirect_uri in its spring.security.oauth2.client.registration:
spring:
security:
oauth2:
client:
registration:
custom:
client-id: ssoClient-2
client-secret: ssoClientSecret-2
scope: read,write,openid
authorization-grant-type: authorization_code
redirect-uri: http://localhost:8084/ui-two/login/oauth2/code/custom
And, of course, we need to have a different server port for it as well, so that we can run them in parallel:
server:
port: 8084
servlet:
context-path: /ui-two
Finally, we’ll tweak the front end HTMLs to have a title as Spring OAuth Client Thymeleaf – 2 instead of – 1 so that we can distinguish between the two.
5. 測試 SSO 行為
為了測試 SSO 行為,讓我們運行我們的應用程序。
我們需要所有 4 個啓動應用程序——授權服務器、資源服務器和兩個客户端應用程序——正常運行。
現在,打開一個瀏覽器,例如 Chrome,並使用憑據 [
同樣,如果用户首先登錄到 Client-2,則無需為 Client-1 輸入用户名/密碼。
6. 結論
在本教程中,我們重點介紹了使用 Spring Security OAuth2 和 Spring Boot,以 Keycloak 作為身份提供者的單點登錄的實現。