1. 概述
在本教程中,我們將探討 Apereo 中央身份驗證服務 (CAS) ,並瞭解 Spring Boot 服務如何利用它進行身份驗證。 CAS 是一種企業級單點登錄 (SSO) 解決方案,並且也是開源的。
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:
- Download the CAS server package from the CAS Project Website.
- Extract the downloaded archive to a directory on your server.
- Navigate to the extracted directory.
Configuration:
-
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.
-
log4j.properties: Configure the logging level for the CAS server. You can adjust this to control the amount of logging output. -
Database Configuration (Optional): If you need to store user information or other data in a database, configure the connection settings in the
cas.jsonfile.
Starting the Server:
- Navigate to the
bindirectory within the CAS server installation directory. - Run the
cas.sh(Linux/macOS) orcas.bat(Windows) script to start the server.
Testing:
- Open a web browser and navigate to the CAS server's URL.
- You should see the CAS login page.
- 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 客户端應用程序。它將包含 Web、Security、Freemarker 和 DevTools 依賴項。此外,我們還將為它的 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=.ftl4. 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 將導致衝突和配置覆蓋。
cas.serviceRegistry.initFromJson=true
cas.serviceRegistry.json.location=classpath:/etc/cas/services5. 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 還有許多可配置的方面,包括主題、協議類型和身份驗證策略等。