使用 Spring Security OAuth2 实现 SSO 单点登录

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、授权服务器

以前,通过 Spring Security OAuth 可以将授权服务器设置为 Spring 应用。

不过,Spring Security OAuth 已被 Spring 弃用,现在可以使用 Keycloak 作为授权服务器。

因此,这次我们在 Spring Boot 应用中把授权服务器设置为嵌入式 Keycloak 服务器。

预配置 中,我们将定义两个客户端,即 ssoClient-1ssoClient-2,分别对应每个客户端应用。

3、资源服务器

接下来,需要一个资源服务器,或者说一个REST API,为客户端应用提供 Foo 资源。

4、客户端应用

客户端应用使用 Spring Boot + Thymeleaf 构建。

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 即可获得包括 Security 在内的所有客户端支持。此外,由于旧的 RestTemplate 将被淘汰,因此我们使用 WebClient,这就是添加 spring-webfluxreactor-netty 的原因。

4.2、Security 配置

接下来是最重要的部分,即第一个客户端应用的 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-idclient-secretscopeauthorization-grant-typeredirect-uri

之后,定义了服务提供商(Service Provider)或授权服务器(同样使用自定义 id),并列出了不同的 URI 供 Spring Security 使用。这就是我们需要定义的全部内容,然后框架就会为我们无缝完成整个登录过程,包括重定向到 Keycloak。

本例中的定义授权服务器,也可以使用其他第三方提供商,如 Facebook 或 GitHub。

4.3、Controller

现在,在客户端应用中实现 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、前端

客户端应用的前端非常简单,使用 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、第二个客户端应用

使用另一个 client_id ssoClient-2 配置第二个应用。

它与第一个应用基本相同。

但是 application.yml 有所不同,在 spring.security.oauth2.client.registration 中包含不同的 client_idclient_secretredirect_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

运行应用,测试 SSO 操作。

在测试中,需要确保四个 Spring Boot 应用 - 授权服务器、资源服务器以及两个客户端应用都已启动并运行。

现在,打开浏览器,使用 john@test.com/123 登录 客户端-1。然后,在另一个窗口或标签页中点击 客户端-2 的 URL。点击登录按钮后,将直接跳转到 Foo 页面,跳过了身份认证步骤。

同样,如果用户先登录 客户端-2,则无需输入用户名/密码即可访问 客户端-1 中的资源。

6、总结

本文介绍了如何使用 Spring Security OAuth2 和 Spring Boot(使用 Keycloak 作为 Identity Provider)实现单点登录。


Ref:https://www.baeldung.com/sso-spring-security-oauth2