1、概览 {#1概览}
本文将带你了解如何使用 Spring Security OAuth 和 Spring Boot 以及 Keycloak 作为授权服务器来实现单点登录(SSO)。
我们会使用 4 个不同的应用:
- 授权服务器 - 中央认证机制
- 资源服务器 -
Foo
资源的提供者 - 两个客户端应用 - 使用 SSO 的应用
简单地说,当用户试图通过一个客户端应用访问资源时,他们会被重定到授权服务器进行身份认证。Keycloak 会对用户进行登录,在登录第一个应用后,如果使用同一浏览器访问第二个客户端应用,用户无需再次输入凭据。
使用 OAuth2 的授权码(Authorization Code)模式。
Spring Security 将此功能称为 OAuth 2.0 登录,而 Spring Security OAuth 将其称为 SSO。
2、授权服务器 {#2授权服务器}
以前,通过 Spring Security OAuth 可以将授权服务器设置为 Spring 应用。
不过,Spring Security OAuth 已被 Spring 弃用,现在可以使用 Keycloak 作为授权服务器。
因此,这次我们在 Spring Boot 应用中把授权服务器设置为嵌入式 Keycloak 服务器。
在 预配置 中,我们将定义两个客户端,即 ssoClient-1
和 ssoClient-2
,分别对应每个客户端应用。
3、资源服务器 {#3资源服务器}
接下来,需要一个资源服务器,或者说一个REST API,为客户端应用提供 Foo
资源。
4、客户端应用 {#4客户端应用}
客户端应用使用 Spring Boot + Thymeleaf 构建。
4.1、Maven 依赖 {#41maven-依赖}
首先,在 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
即可获得包括 Security 在内的所有客户端支持。此外,由于旧的 RestTemplate
将被淘汰,因此我们使用 WebClient
,这就是添加 spring-webflux
和 reactor-netty
的原因。
4.2、Security 配置 {#42security-配置}
接下来是最重要的部分,即第一个客户端应用的 Security 配置:
@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();
}
}
该配置的核心部分是 oauth2Login()
方法,用于启用 Spring Security 的 OAuth 2.0 登录支持。由于我们使用的是 Keycloak,它默认情况下是 Web 应用和 RESTful Web 服务的单点登录解决方案,因此无需为 SSO 添加任何其他配置。
最后,还定义了一个 WebClient
Bean,作为简单的 HTTP 客户端来处理发送到资源服务器的请求。
添加 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/
spring.security.oauth2.client.registration
是注册客户端的根命名空间。我们定义了一个客户端,注册 id 为 custom
。然后,定义了其 client-id
、client-secret
、scope
、authorization-grant-type
和 redirect-uri
。
之后,定义了服务提供商(Service Provider)或授权服务器(同样使用自定义 id),并列出了不同的 URI 供 Spring Security 使用。这就是我们需要定义的全部内容,然后框架就会为我们无缝完成整个登录过程,包括重定向到 Keycloak。
本例中的定义授权服务器,也可以使用其他第三方提供商,如 Facebook 或 GitHub。
4.3、Controller {#43controller}
现在,在客户端应用中实现 Conroller,向资源服务器请求 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、前端 {#44前端}
客户端应用的前端非常简单,使用 Thymeleaf
模板引擎。
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、第二个客户端应用 {#45第二个客户端应用}
使用另一个 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,openid
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、测试 SSO {#5测试-sso}
运行应用,测试 SSO 操作。
在测试中,需要确保四个 Spring Boot 应用 - 授权服务器、资源服务器以及两个客户端应用都已启动并运行。
现在,打开浏览器,使用 john@test.com/123
登录 客户端-1 。然后,在另一个窗口或标签页中点击 客户端-2 的 URL。点击登录按钮后,将直接跳转到 Foo
页面,跳过了身份认证步骤。
同样,如果用户先登录 客户端-2 ,则无需输入用户名/密码即可访问 客户端-1 中的资源。
6、总结 {#6总结}
本文介绍了如何使用 Spring Security OAuth2 和 Spring Boot(使用 Keycloak 作为 Identity Provider)实现单点登录。
Ref:https://www.baeldung.com/sso-spring-security-oauth2