51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

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

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-1ssoClient-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-webfluxreactor-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-idclient-secretscopeauthorization-grant-typeredirect-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_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 {#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

赞(0)
未经允许不得转载:工具盒子 » 使用 Spring Security OAuth2 实现 SSO 单点登录