1. 概述
本教程将介绍如何使用 Spring Security OAuth 和 Spring Boot 实现 SSO(单点登录),并采用 Keycloak 作为授权服务器。
我们将使用4个独立应用:
- 授权服务器:核心认证机制
- 资源服务器:提供 Foo 资源的服务
- 两个客户端应用:使用SSO的应用
简单来说,当用户通过第一个客户端应用访问资源时,会被重定向到授权服务器进行身份验证。Keycloak 完成登录后,如果用户在同一浏览器中访问第二个客户端应用,无需再次输入凭据即可直接访问。
我们将使用 OAuth2 的授权码(Authorization Code)模式驱动认证委托。
本文基于 Spring Security 5 的 OAuth 新栈,若需使用旧版 Spring Security OAuth,可参考这篇旧文:Simple Single Sign-On with Spring Security OAuth2 (legacy stack)
根据迁移指南:
Spring Security 将此功能称为 OAuth 2.0 Login,而 Spring Security OAuth 称其为 SSO
2. 授权服务器
过去 Spring Security OAuth 栈支持将授权服务器设置为 Spring 应用,但该栈已被弃用。现在我们使用 Keycloak 作为授权服务器。
**本次我们将授权服务器设置为嵌入在 Spring Boot 应用中的 Keycloak 服务器**。
在预配置中,**我们定义了两个客户端:ssoClient-1
和 ssoClient-2
**,分别对应两个客户端应用。
3. 资源服务器
接下来需要资源服务器,即提供 Foo 资源的 REST API,供客户端应用消费。
其实现本质上与之前 Angular 客户端使用的版本相同。
4. 客户端应用
现在来看 Thymeleaf 客户端应用,我们使用 Spring Boot 最小化配置。
注意需要准备两个此类应用以演示 SSO 功能。
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 {
@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 服务的单点登录解决方案,无需额外配置 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
(需与授权服务器配置一致)。
接着定义了服务提供商(即授权服务器),同样使用 ID custom
,并列出 Spring Security 需要使用的各种 URI。框架会自动处理整个登录流程,包括重定向到 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
页面要求用户身份验证。未认证用户访问时会被重定向到 Keycloak 登录页。
4.5. 第二个客户端应用
配置第二个应用 Spring OAuth Client Thymeleaf -2
,使用另一个 client_id
ssoClient-2
。
配置与第一个应用基本相同,但 application.yml
需修改以下内容:
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
以区分两个应用。
5. 测试 SSO 行为
启动所有应用测试 SSO 功能:
- 授权服务器
- 资源服务器
- 两个客户端应用
在浏览器(如 Chrome)中访问客户端1,使用凭据 user@example.com/123
登录。接着在新标签页访问客户端2,点击登录按钮后将直接跳转到 Foos 页面,无需重复认证。
同样,若先登录客户端2,访问客户端1时也无需输入用户名/密码。
6. 总结
本教程重点介绍了使用 Spring Security OAuth2 和 Spring Boot 实现 SSO,并以 Keycloak 作为身份提供者。
完整源代码请查阅 GitHub 仓库。