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-1ssoClient-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-webfluxreactor-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-idclient-secretscopeauthorization-grant-typeredirect-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 仓库


原始标题:Simple Single Sign-On with Spring Security OAuth2 | Baeldung

» 下一篇: Structurizr 入门指南