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
 51工具盒子
51工具盒子 
                 
                             
                         
                         
                         
                        