Spring Security OAuth2 簡易單點登錄

Spring Security
Remote
1
12:19 AM · Nov 30 ,2025

1. 概述在本教程中,我們將討論如何使用 Spring Security OAuth 和 Spring Boot,藉助 Keycloak 作為授權服務器,實現 單點登錄 (SSO) 的實施方法。

我們將使用 4 個獨立應用程序:

    授權服務器,它是中心身份驗證機制。
  • 資源服務器,它是 Foo 提供者。
  • 客户端應用程序,這些應用程序使用 SSO。
  • 簡單來説,當用户通過一個客户端應用程序訪問資源時,他們將被重定向到首先進行身份驗證,通過授權服務器。Keycloak 會將用户登錄,即使在第一個應用程序仍然登錄的情況下,如果使用相同的瀏覽器訪問第二個客户端應用程序,用户就不需要再次輸入憑據。

    我們將使用 OAuth2 中的 授權碼 grant 類型來驅動身份驗證的委託。

    我們將使用 Spring Security 5 中的 OAuth 棧。 如果您想使用 Spring Security OAuth 的遺留棧,請查看這篇文章:Simple Single Sign-On with Spring Security OAuth2 (legacy stack)

    根據 遷移指南Spring Security 將此功能稱為 OAuth 2.0 登錄,而 Spring Security OAuth 則將其稱為 SSO

    好了,讓我們直接開始吧。

    2. 授權服務器此前,Spring Security OAuth 堆棧提供了將授權服務器設置為 Spring Application 的可能性。

    然而,Spring 已經廢棄了 OAuth 堆棧,現在我們將使用 Keycloak 作為我們的授權服務器。

    因此,我們現在將授權服務器設置為 Spring Boot 應用中的嵌入式 Keycloak 服務器。

    在我們的預配置中,我們將定義兩個客户端,ssoClient-1ssoClient-2,一個用於每個客户端應用程序。

    3. 資源服務器

    接下來,我們需要一個資源服務器,或者説是 REST API,它將為我們的客户端應用程序提供 Foo

    它與我們之前為 Angular 客户端應用程序所使用的基本相同。

    4. The Client Applications

    Now let’s look at our Thymeleaf Client Application; we’ll, of course, use Spring Boot to minimize the configuration.

    Do keep in mind that we’ll need to have 2 of these to demonstrate Single Sign-On functionality.

    4.1. Maven Dependencies

    First, we will need the following dependencies in our 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>
    

    To include all the client support we’ll require, including security, we just need to add spring-boot-starter-oauth2-client.

    Also, since the old RestTemplate is going to be deprecated, we’re going to use WebClient, and that’s why we added spring-webflux and reactor-netty.

    4.2. Security Configuration

    Next, the most important part, the security configuration of our first client application:

    @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();
        }
    
    }

    The core part of this configuration is the oauth2Login() method, which is used to enable Spring Security’s OAuth 2.0 Login support.

    Since we’re using Keycloak, which is by default a single sign-on solution for web apps and RESTful web services, we do not need to add any further configuration for SSO.

    Finally, we also defined a WebClient bean to act as a simple HTTP Client to handle requests to be sent to our Resource Server.

    And here’s the 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/        
    

    Here, spring.security.oauth2.client.registration is the root namespace for registering a client. We defined a client with registration id custom. Then we defined its client-id, client-secret, scope, authorization-grant-type and redirect-uri, which of course, should be the same as that defined for our Authorization Server.

    After that, we defined our service provider or the Authorization Server, again with the same id of custom, and listed down its different URI’s for Spring Security to use. That’s all we need to define, and the framework does the entire logging-in process, including redirection to Keycloak, seamlessly for us.

    Also note that, in our example here, we rolled out our Authorization Server, but of course we can also use other, third-party providers such as Facebook or GitHub.

    4.3. The Controller

    Let’s now implement our controller in the Client App to ask for Foos from our Resource Server:

    @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";
        }
    }

    As we can see, we have only one method here that’ll dish out the resources to the foos template. We did not have to add any code for login.

    4.4. Front End

    Now, let’s take a look at the front-end configuration of our client application. We’re not going to focus on that here, mainly because we already covered in on the site.

    Our client application here has a very simple front-end; here’s the 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>

    And the 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>

    The foos.html page needs the users to be authenticated. If a non-authenticated user tries to access foos.html, they’ll be redirected to Keycloak’s login page first.

    4.5. The Second Client Application

    We’ll configure a second application, Spring OAuth Client Thymeleaf -2 using another client_id ssoClient-2.

    It’ll mostly be the same as the first application we just described.

    The application.yml will differ to include a different client_id, client_secret and redirect_uri in its spring.security.oauth2.client.registration:

    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

    And, of course, we need to have a different server port for it as well, so that we can run them in parallel:

    server: 
      port: 8084
      servlet: 
        context-path: /ui-two

    Finally, we’ll tweak the front end HTMLs to have a title as Spring OAuth Client Thymeleaf – 2 instead of – 1 so that we can distinguish between the two.

    5. 測試 SSO 行為

    為了測試 SSO 行為,讓我們運行我們的應用程序。

    我們需要所有 4 個啓動應用程序——授權服務器、資源服務器和兩個客户端應用程序——正常運行。

    現在,打開一個瀏覽器,例如 Chrome,並使用憑據 [Client-1。 接下來,在另一個窗口或標籤頁中,訪問 Client-2 的 URL。 點擊登錄按鈕後,我們將立即被重定向到 Foos 頁面,從而跳過身份驗證步驟。

    同樣,如果用户首先登錄到 Client-2,則無需為 Client-1 輸入用户名/密碼。

    6. 結論

    在本教程中,我們重點介紹了使用 Spring Security OAuth2 和 Spring Boot,以 Keycloak 作為身份提供者的單點登錄的實現。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.