1. 概述
CAS is an enterprise Single Sign-On (SSO) solution that is also open source.2. CAS Server Setup
2.1. CAS Installation and Dependencies
服務器使用 Maven (Gradle) War Overlay 樣式以簡化設置和部署:
git clone https://github.com/apereo/cas-overlay-template.git cas-server
此命令將 cas-overlay-template 克隆到 cas-server 目錄。
我們將涵蓋的方面包括 JSON 服務註冊和 JDBC 數據庫連接。因此,我們將向 dependencies 部分的 build.gradle 文件添加這些模塊:
compile "org.apereo.cas:cas-server-support-json-service-registry:${casServerVersion}"
compile "org.apereo.cas:cas-server-support-jdbc:${casServerVersion}"
請務必檢查 casServer 的最新版本。
2.2. CAS Server Configuration
在啓動 CAS 服務器之前,我們需要添加一些基本配置。我們首先創建一個 cas-server/src/main/resources 文件夾,並在該文件夾中,隨後我們創建 application.properties 文件:
server.port=8443
spring.main.allow-bean-definition-overriding=true
server.ssl.key-store=classpath:/etc/cas/thekeystore
server.ssl.key-store-password=changeit
讓我們創建 referenced 的 key-store 文件。首先,我們需要創建 /etc/cas 和 /etc/cas/config 文件夾,在 cas-server/src/main/resources 文件夾中。
然後,我們需要將目錄更改為 cas-server/src/main/resources/etc/cas,並運行命令以生成 thekeystore:
keytool -genkey -keyalg RSA -alias thekeystore -keystore thekeystore -storepass changeit -validity 360 -keysize 2048
為了避免 SSL 握手錯誤,我們應該使用 localhost 作為第一個和最後一個名稱的值。我們應該使用相同的組織名稱和單元值。此外,我們應該將 thekeystore 導入到我們用於運行客户端應用程序的 JDK/JRE 中:
keytool -importkeystore -srckeystore thekeystore -destkeystore $JAVA11_HOME/jre/lib/security/cacerts
源和目標 keystore 的密碼是 changeit。在 Unix 系統上,我們可能需要使用管理員權限(sudo)運行此命令。導入後,我們應該重啓所有 Java 實例,或者重啓系統。
我們使用 JDK11,因為它由 CAS 版本 6.1.x 需求。我們還定義了環境變量 $JAVA11_HOME,該變量指向其主目錄。現在我們可以啓動 CAS 服務器:
./gradlew[.bat] run -Dorg.gradle.java.home=$JAVA11_HOME
當應用程序啓動時,我們將看到“READY”打印在終端上,並且服務器將在 https://localhost:8443 可用。
2.3. CAS Server User Configuration
我們還沒有配置任何用户,因此我們還不能登錄,因為我們還沒有配置任何用户。CAS 具有不同的配置方法,包括獨立模式。讓我們創建一個配置文件夾 cas-server/src/main/resources/etc/cas/config,並在該文件夾中創建一個 properties 文件 cas.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 依賴。此外,我們還將添加 Spring Security CAS 模塊到其 pom.xml 中:
<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 屬性應是唯一的。換句話説,不應有相同的 id 註冊到同一個 CAS 服務器的兩個或多個服務。具有重複的 id 將導致衝突和配置覆蓋。
cas.serviceRegistry.initFromJson=true
cas.serviceRegistry.json.location=classpath:/etc/cas/services
5. CAS Client Single Sign-On Configuration
full flow of interactions, called a CAS sequence." lang="en">The next step for us is to configure Spring Security to work with the CAS server. We should also check the full flow of interactions, called a CAS sequence.
CasSecuredApplication class of our Spring Boot app:" lang="en">Let’s add the following bean configurations to the CasSecuredApplication class of our Spring Boot app:
@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;
}
ServiceProperties bean has the same URL as the serviceId in casSecuredApp-8900.json. This is important because it identifies this client to the CAS server." lang="en">The ServiceProperties bean has the same URL as the serviceId in casSecuredApp-8900.json. This is important because it identifies this client to the CAS server." lang="en">)
sendRenew property of ServiceProperties is set to false. This means a user only needs to present login credentials to the server once." lang="en">The sendRenew property of ServiceProperties is set to false. This means a user only needs to present login credentials to the server once." lang="en">)
AuthenticationEntryPoint bean will handle authentication exceptions. Thus, it’ll redirect the user to the login URL of the CAS server for authentication." lang="en">The AuthenticationEntryPoint bean will handle authentication exceptions. Thus, it’ll redirect the user to the login URL of the CAS server for authentication." lang="en">)
In summary, the authentication flow goes:
- A user attempts to access a secure page, which triggers an authentication exception.
- AuthenticationEntryPoint. In response, the AuthenticationEntryPoint will take the user to the CAS server login page – https://localhost:8443/login.
- On successful authentication, the server redirects back to the client with a ticket.
- CasAuthenticationFilter will pick up the redirect and call CasAuthenticationProvider." lang="en">CasAuthenticationFilter will pick up the redirect and call CasAuthenticationProvider.
- CasAuthenticationProvider will use TicketValidator to confirm the presented ticket on CAS server." lang="en">CasAuthenticationProvider will use TicketValidator to confirm the presented ticket on CAS server.
- If the ticket is valid, the user will get a redirection to the requested secure URL.
HttpSecurity to secure some routes in WebSecurityConfig. In the process, we’ll also add the authentication entry point for exception handling." lang="en">Finally, let’s configure HttpSecurity to secure some routes in WebSecurityConfig. In the process, we’ll also add the authentication entry point for exception handling." lang="en">)
@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;
}
logoutFilter 將攔截指向 /logout/cas 的請求,並將應用程序重定向到 CAS 服務器。 SingleSignOutFilter 將攔截來自 CAS 服務器的請求,並執行本地註銷。
7. Connecting the CAS Server to a Database
我們可以配置 CAS 服務器讀取憑據來自 MySQL 數據庫。 我們將使用 MySQL 服務器上運行的 test 數據庫。 讓我們更新 cas-server/src/main/resources/application.yml:
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
此外,請在 cas-secured-app 中進行配置 cas-secured-app/src/main/resources/application.properties:
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
We set the 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;}
CasAuthenticationProvider 需要一個 UserDetailsService 來根據 CAS 票據加載用户詳細信息。 UserDetailsService 負責從數據源檢索用户信息,例如數據庫。 在 loadUserByUsername 方法中 UserDetailsService 實現,您可以自定義邏輯以根據提供的用户名加載用户詳細信息。
CasUserDetailsService 類中的 loadUserByUsername 方法負責根據用户名加載用户的詳細信息。 您可以找到有關使用數據庫後向後填充用户詳細信息的有關身份驗證的信息。
當 CAS 票據驗證成功,用户詳細信息加載後,CasAuthenticationProvider 創建了一個身份驗證的 Authentication 對象,該對象可以隨後在應用程序中用於授權和訪問控制。
CasAuthenticationProvider 不使用密碼進行身份驗證。 儘管如此,它的用户名必須與 CAS 服務器的用户名匹配,才能進行身份驗證成功。 CAS 服務器需要一個在 localhost 上運行的 MySQL 服務器,端口為 3306。 用户名和密碼應為 root。
請重啓 CAS 服務器和 Spring Boot 應用程序。 然後使用新的憑據進行身份驗證。
8. 結論
我們已經探討了如何使用 CAS SSO 與 Spring Security 以及相關的配置文件的使用方法。CAS SSO 還有許多可配置的方面,包括主題、協議類型和身份驗證策略等。