知識庫 / Spring / Spring Security RSS 訂閱

Spring Security 與 CAS 單點登錄

Spring Security
HongKong
10
02:12 PM · Dec 06 ,2025

1. 概述

在本教程中,我們將探討 Apereo 中央身份驗證服務 (CAS) ,並瞭解 Spring Boot 服務如何利用它進行身份驗證。 CAS 是一種企業級單點登錄 (SSO) 解決方案,並且也是開源的。

什麼是 SSO? 當您使用相同的憑據登錄 YouTube、Gmail 和 Maps 時,這就是單點登錄。 我們將通過設置 CAS 服務器和 Spring Boot 應用程序來演示這一點。 Spring Boot 應用程序將使用 CAS 進行身份驗證。

2. CAS 服務器設置

This section describes the steps involved in setting up a CAS (Central Authentication Service) server. It assumes you have a basic understanding of authentication and authorization concepts.

Prerequisites:

  • A server with a supported operating system (e.g., Linux, Windows Server).
  • Java Development Kit (JDK) 8 or later installed.
  • A web browser for testing.

Installation:

  1. Download the CAS server package from the CAS Project Website.
  2. Extract the downloaded archive to a directory on your server.
  3. Navigate to the extracted directory.

Configuration:

  1. cas.json: This file contains the configuration settings for the CAS server. You will need to modify this file to specify the server's URL, SSL settings, and other parameters. Key settings include:

    • serverUrl: The URL of your CAS server (e.g., https://cas.example.com).
    • ssl: Enable or disable SSL/TLS communication.
    • subjectNameAttribute: The name of the attribute that contains the user's username.
    • authenticationAttributes: A list of attributes that are required for authentication.
  2. log4j.properties: Configure the logging level for the CAS server. You can adjust this to control the amount of logging output.

  3. Database Configuration (Optional): If you need to store user information or other data in a database, configure the connection settings in the cas.json file.

Starting the Server:

  1. Navigate to the bin directory within the CAS server installation directory.
  2. Run the cas.sh (Linux/macOS) or cas.bat (Windows) script to start the server.

Testing:

  1. Open a web browser and navigate to the CAS server's URL.
  2. You should see the CAS login page.
  3. Enter your username and password to authenticate.

Troubleshooting:

  • Check the server logs for error messages.
  • Verify that the CAS server is accessible from the browser.
  • Ensure that the firewall is not blocking access to the CAS server.

2.1. CAS 安裝與依賴

服務器使用 Maven (Gradle) War Overlay 樣式,以簡化設置和部署:

git clone https://github.com/apereo/cas-overlay-template.git cas-server

這條命令會將 cas-overlay-template 克隆到 cas-server 目錄下。

我們將涵蓋的內容包括 JSON 服務註冊和 JDBC 數據庫連接。因此,我們將向 build.gradle 文件的 dependencies 部分添加它們的模塊:

compile "org.apereo.cas:cas-server-support-json-service-registry:${casServerVersion}"
compile "org.apereo.cas:cas-server-support-jdbc:${casServerVersion}"

讓我們確保檢查最新版本的 casServer

2.2. CAS 服務器配置

在啓動 CAS 服務器之前,我們需要添加一些基本配置。首先,我們創建一個 <em >cas-server/src/main/resources</em > 文件夾,並在該文件夾中,隨後創建 <em >application.properties</em > 文件:

server.port=8443
spring.main.allow-bean-definition-overriding=true
server.ssl.key-store=classpath:/etc/cas/thekeystore
server.ssl.key-store-password=changeit
<div>
  <div>
    <p>接下來,我們創建配置中引用的密鑰庫文件。首先,我們需要在 <em>/etc/cas</em> 和 <em>/etc/cas/config</em> 目錄下創建文件夾,位於 <em>cas-server/src/main/resources</em>。</p>
    <p>然後,我們需要將目錄更改為 <em>cas-server/src/main/resources/etc/cas</em>,並運行生成 <em>thekeystore</em> 的命令:</p>
  </div>
</div>
keytool -genkey -keyalg RSA -alias thekeystore -keystore thekeystore -storepass changeit -validity 360 -keysize 2048

為了避免出現 SSL Handshake 錯誤,我們應該將 first name 和 last name 的值設置為 localhost。 同樣,我們應該將組織名稱和單位也設置為該值。 此外,還需要將 thekeystore 導入到我們將用於運行客户端應用程序的 JDK/JRE 中:

keytool -importkeystore -srckeystore thekeystore -destkeystore $JAVA11_HOME/jre/lib/security/cacerts
<div>
 <div>
  <p>源密鑰庫和目標密鑰庫的密碼是 <em><strong >changeit</strong></em>。 在 Unix 系統上,可能需要使用管理員權限 (<em >sudo</em>) 運行此命令。 導入後,應重啓所有正在運行的 Java 實例或重啓系統。</p>
  <p>我們使用 JDK11,因為它由 CAS 版本 6.1.x 必需。 此外,我們已定義環境變量 $JAVA11_HOME 指向其主目錄。 現在,我們可以啓動 CAS 服務器:</p>
 </div>
</div>
./gradlew[.bat] run -Dorg.gradle.java.home=$JAVA11_HOME

應用程序啓動時,終端上會打印“READY”,並且服務器將在 https://localhost:8443.

2.3. CAS 服務器用户配置

我們目前還無法登錄,因為我們尚未配置任何用户。CAS 提供了多種管理配置的方法,包括獨立模式。讓我們創建一個配置文件夾 cas-server/src/main/resources/etc/cas/config,並在其中創建一個 properties 文件 cas.properties。現在,我們可以定義 properties 文件中的一個靜態用户:

cas.authn.accept.users=casuser::Mellon

為了使配置文件夾的位置能夠傳遞給 CAS 服務器,以便設置生效,我們必須更新 tasks.gradle,以便從命令行將位置作為 JVM 參數傳遞:

task run(group: "build", description: "Run the CAS web application in embedded container mode") {
    dependsOn 'build'
    doLast {
        def casRunArgs = new ArrayList<>(Arrays.asList(
          "-server -noverify -Xmx2048M -XX:+TieredCompilation -XX:TieredStopAtLevel=1".split(" ")))
        if (project.hasProperty('args')) {
            casRunArgs.addAll(project.args.split('\\s+'))
        }
        javaexec {
            main = "-jar"
            jvmArgs = casRunArgs
            args = ["build/libs/${casWebApplicationBinaryName}"]
            logger.info "Started ${commandLine}"
        }
    }
}

我們隨後保存文件並運行:

./gradlew run
  -Dorg.gradle.java.home=$JAVA11_HOME
  -Pargs="-Dcas.standalone.configurationDirectory=/cas-server/src/main/resources/etc/cas/config"

請注意,cas.standalone.configurationDirectory 的值是一個絕對路徑。 我們可以現在訪問 https://localhost:8443 並使用用户名 casuser 和密碼 Mellon 進行登錄。

3. CAS 客户端配置

我們將使用 Spring Initializr 生成一個 Spring Boot 客户端應用程序。它將包含 WebSecurityFreemarkerDevTools 依賴項。此外,我們還將為它的 pom.xml 添加 Spring Security CAS 模塊的依賴項:

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-cas</artifactId>
    <versionId>5.3.0.RELEASE</versionId>
</dependency>

最後,讓我們添加以下 Spring Boot 屬性來配置應用程序:

server.port=8900
spring.freemarker.suffix=.ftl

4. CAS 服務器服務註冊

客户端應用程序必須在進行任何身份驗證之前,與 CAS 服務器進行註冊。 CAS 服務器支持使用 YAML、JSON、MongoDB 和 LDAP 客户端註冊表。

在本教程中,我們將使用 JSON 服務註冊表方法。讓我們創建一個新的文件夾 cas-server/src/main/resources/etc/cas/services。 恰恰是這個文件夾將包含服務註冊表 JSON 文件。

我們將創建一個 JSON 文件,其中包含客户端應用程序的定義。 該文件的名稱 casSecuredApp-8900.json 遵循 serviceName-Id.json 模式:

{
  "@class" : "org.apereo.cas.services.RegexRegisteredService",
  "serviceId" : "http://localhost:8900/login/cas",
  "name" : "casSecuredApp",
  "id" : 8900,
  "logoutType" : "BACK_CHANNEL",
  "logoutUrl" : "http://localhost:8900/exit/cas"
}

serviceId 屬性定義了客户端應用程序的正則表達式 URL 模式。該模式應與客户端應用程序的 URL 匹配。

id 屬性應唯一。換句話説,不應在同一個 CAS 服務器上註冊具有相同的id 的兩個或多個服務。 具有重複的id 將導致衝突和配置覆蓋。

我們還配置註銷類型為 BACK_CHANNEL,URL 為 http://localhost:8900/exit/cas,以便我們能夠進行單點註銷。
在 CAS 服務器能夠使用我們的 JSON 配置文件之前,我們必須在 cas.properties 中啓用 JSON 註冊表:
cas.serviceRegistry.initFromJson=true
cas.serviceRegistry.json.location=classpath:/etc/cas/services

5. CAS 客户端單點登錄配置接下來,我們需要配置 Spring Security 與 CAS 服務器協同工作。 此外,我們還應查看 完整交互流程,即 CAS 序列。

以下 Bean 配置將添加到我們 Spring Boot 應用中的 CasSecuredApplication 類中:

@Bean
public CasAuthenticationFilter casAuthenticationFilter(
  AuthenticationManager authenticationManager,
  ServiceProperties serviceProperties) throws Exception {
    CasAuthenticationFilter filter = new CasAuthenticationFilter();
    filter.setAuthenticationManager(authenticationManager);
    filter.setServiceProperties(serviceProperties);
    return filter;
}

@Bean
public ServiceProperties serviceProperties() {
    logger.info("service properties");
    ServiceProperties serviceProperties = new ServiceProperties();
    serviceProperties.setService("http://cas-client:8900/login/cas");
    serviceProperties.setSendRenew(false);
    return serviceProperties;
}

@Bean
public TicketValidator ticketValidator() {
    return new Cas30ServiceTicketValidator("https://localhost:8443");
}

@Bean
public CasAuthenticationProvider casAuthenticationProvider(
  TicketValidator ticketValidator,
  ServiceProperties serviceProperties) {
    CasAuthenticationProvider provider = new CasAuthenticationProvider();
    provider.setServiceProperties(serviceProperties);
    provider.setTicketValidator(ticketValidator);
    provider.setUserDetailsService(
      s -> new User("[email protected]", "Mellon", true, true, true, true,
      AuthorityUtils.createAuthorityList("ROLE_ADMIN")));
    provider.setKey("CAS_PROVIDER_LOCALHOST_8900");
    return provider;
}
<div>
  <div>
    <p><em>ServiceProperties</em> Bean 與 <em>serviceId</em> 在 <em>casSecuredApp-8900.json</em> 中的值相同。 這很重要,因為它將此客户端標識給 CAS 服務器。</p>
    <p><em>ServiceProperties</em> 中的 <em>sendRenew</em> 屬性設置為 <em>false</em>。 這意味着用户只需向服務器呈現登錄憑據一次。</p>
    <p><em>AuthenticationEntryPoint</em> Bean 將處理身份驗證異常。 因此,它會將用户重定向到 CAS 服務器的登錄 URL 以進行身份驗證。</p>
    <p>總結一下,身份驗證流程如下:</p>
    <ol>
      <li>用户嘗試訪問安全頁面,這會觸發身份驗證異常</li>
      <li>異常會觸發 <em>AuthenticationEntryPoint</em>。 響應中,<em>AuthenticationEntryPoint</em> 將用户帶到 CAS 服務器登錄頁面 – <a href="https://localhost:8443/login">https://localhost:8443/login</a></li>
      <li>在成功身份驗證後,服務器會將帶有票據的客户端重定向回</li>
      <li><em>CasAuthenticationFilter</em> 將捕獲重定向並調用 <em>CasAuthenticationProvider</em></li>
      <li><em>CasAuthenticationProvider</em> 將使用 <em>TicketValidator</em> 來確認在 CAS 服務器上呈現的票據</li>
      <li>如果票據有效,用户將獲得重定向到請求的受保護 URL</li>
    </ol>
  </div>
</div>

<p>最後,讓我們配置 <em>HttpSecurity</em> 以安全一些路由,在 <em>WebSecurityConfig</em> 中。 在此過程中,我們還將添加身份驗證入口點用於異常處理:</p>
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests().antMatchers( "/secured", "/login").authenticated()
      .and()
      .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint())
      .and()
      .addFilterBefore(singleSignOutFilter, CasAuthenticationFilter.class)
}

6. CAS 客户端單註銷配置

此前,我們已經討論了單點登錄;現在,讓我們考慮 CAS 的單註銷 (SLO)。

使用 CAS 管理用户身份驗證的應用可以從兩個地方註銷用户:

  • 客户端應用可以從本地註銷用户,這不會影響使用相同 CAS 服務器的其他應用中的用户登錄狀態。
  • 客户端應用還可以從 CAS 服務器註銷用户,這將導致所有連接到相同 CAS 服務器的客户端應用中的用户註銷。

我們首先將實現客户端上的註銷,然後將其擴展到 CAS 服務器上的單註銷。

為了讓人們清楚地瞭解幕後發生的事情,我們將創建一個 logout() 方法來處理本地註銷。在成功的情況下,它會將我們重定向到一個包含單註銷鏈接的頁面:

@GetMapping("/logout")
public String logout(
  HttpServletRequest request, 
  HttpServletResponse response, 
  SecurityContextLogoutHandler logoutHandler) {
    Authentication auth = SecurityContextHolder
      .getContext().getAuthentication();
    logoutHandler.logout(request, response, auth );
    new CookieClearingLogoutHandler(
      AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY)
      .logout(request, response, auth);
    return "auth/logout";
}

在單註銷過程中,CAS服務器首先會過期用户的票據,然後向所有已註冊的客户端應用發送異步請求。每個接收到此信號的客户端應用都會執行本地註銷。 這樣就實現了一次註銷,導致所有地方註銷。

説起這一點,我們為我們的客户端應用添加一些 Bean 配置。 尤其是在 CasSecuredApplicaiton 中:

@Bean
public SecurityContextLogoutHandler securityContextLogoutHandler() {
    return new SecurityContextLogoutHandler();
}

@Bean
public LogoutFilter logoutFilter() {
    LogoutFilter logoutFilter = new LogoutFilter("https://localhost:8443/logout",
      securityContextLogoutHandler());
    logoutFilter.setFilterProcessesUrl("/logout/cas");
    return logoutFilter;
}

@Bean
public SingleSignOutFilter singleSignOutFilter() {
    SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter();
    singleSignOutFilter.setLogoutCallbackPath("/exit/cas");
    singleSignOutFilter.setIgnoreInitConfiguration(true);
    return singleSignOutFilter;
}
<div>
  <div>
    <p><em>logoutFilter</em> 將攔截到 <em>/logout/cas</em> 的請求,並將應用程序重定向到 CAS 服務器。<em>SingleSignOutFilter</em> 將攔截來自 CAS 服務器的請求,並執行本地註銷。</p>
  </div>
</div>

7. 將 CAS 服務器連接到數據庫

我們可以配置 CAS 服務器從 MySQL 數據庫讀取憑據。我們將使用本地機器上運行的 MySQL 服務器的 <em >test</em> 數據庫。 讓我們更新 <em >cas-server/src/main/resources/application.yml</em>

cas:
    authn:
        accept:
            users:
        jdbc:
            query[0]:
                sql: SELECT * FROM users WHERE email = ?
                url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
                dialect: org.hibernate.dialect.MySQLDialect
                user: root
                password: root
                ddlAuto: none
                driverClass: com.mysql.cj.jdbc.Driver
                fieldPassword: password
                passwordEncoder:
                    type: NONE
<div>
  <div>
    <p>此外,請在 <strong title="cas-secured-app">cas-secured-app</strong> 中進行相同的配置:<em title="cas-secured-app">cas-secured-app/src/main/resources/application.properties</em>:</p>
  </div>
</div>
spring.jpa.generate-ddl=false
spring.datasource.url= jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

我們設置了cas.authn.accept.users為空。這將禁用 CAS 服務器使用靜態用户倉庫

根據上述 SQL,用户的憑據存儲在 users 表中。 email 列代表用户的 principal (username)。

請務必查看 支持的數據庫列表、可用驅動程序和方言。 我們還設置了密碼編碼器類型為 NONE。 其他 加密機制及其特定屬性也可用。

請注意,CAS 服務器數據庫中的用户必須與客户端應用程序中的用户相同。

讓我們更新 CasAuthenticationProvider,使其用户名與 CAS 服務器相同:

@Bean
public CasUserDetailsService getUser(){
    return new CasUserDetailsService();
}

@Bean
public CasAuthenticationProvider casAuthenticationProvider(
  TicketValidator ticketValidator,
  ServiceProperties serviceProperties) {
    CasAuthenticationProvider provider = new CasAuthenticationProvider();
    provider.setServiceProperties(serviceProperties);
    provider.setTicketValidator(ticketValidator);
    provider.setUserDetailsService(getUser());
    provider.setKey("CAS_PROVIDER_LOCALHOST_8900");
    return provider;
}
<div>
  <em >CasAuthenticationProvider</em> 需要一個 <em >UserDetailsService</em> 來根據 CAS 票據加載用户詳細信息。 <em >UserDetailsService</em> 負責從數據源(如數據庫)檢索用户信息。 在 <em >loadUserByUsername</em> 方法中,你可以自定義邏輯以根據提供的用户名加載用户詳細信息。
</div>
public class CasUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // Get the user from the database.
        CasUser casUser = getUserFromDatabase(username);

        // Create a UserDetails object.
        UserDetails userDetails = new User(
            casUser.getEmail(),
            casUser.getPassword(),
           Collections.singletonList(new SimpleGrantedAuthority("ROLE_ADMIN")));

        return userDetails;
    }

    private CasUser getUserFromDatabase(String username) {
       return userRepository.findByEmail(username);
    }
}

`loadUserByUsername` 方法是 `CasUserDetailsService` 類的一部分。該方法負責根據用户名加載用户的詳細信息。有關使用數據庫後向後 UserDetailsService 的更多信息,請參考相關文檔。

在 CAS 票據驗證並通過後,以及加載用户詳細信息後,`CasAuthenticationProvider` 創建一個認證的 `Authentication` 對象,該對象可用於在應用程序中進行授權和訪問控制。

CasAuthenticationProvider 不使用密碼進行身份驗證。儘管如此,它的用户名必須與 CAS 服務器的用户名匹配,才能進行身份驗證。CAS 服務器需要一個 MySQL 服務器在 localhost 上運行,端口為 3306。用户名和密碼應為 root

再次重啓 CAS 服務器和 Spring Boot 應用。然後使用新的憑據進行身份驗證。

8. 結論

我們已經探討了如何使用 CAS SSO 與 Spring Security 以及相關的配置文件的集成。CAS SSO 還有許多可配置的方面,包括主題、協議類型和身份驗證策略等。

發佈 評論

Some HTML is okay.