本文目标
基于SpringBoot + Maven 分别使用自動配置與手動配置過濾器方式實作CAS用戶端登出及單點登出。
本文基于《CAS學習筆記三:SpringBoot自動/手動配置方式內建CAS單點登入》的代碼擴充而來,完整代碼見 https://github.com/hellxz/cas-integration-demo
CAS服務端配置
單點登出跟随
service
給出的跳轉位址重定向功能 在 CAS 服務端預設是關閉的,是以需要先開啟它。
vim webapps/cas/WEB-INF/classes/application.properties
在最下方追加配置項
cas.logout.followServiceRedirects=true
,儲存重新開機CAS服務端。
代碼目錄結構
以上紅字僅對本文修改的部分進行說明,其餘請參考之前單點登入的實作文章。
代碼實作
僅增量介紹關鍵類
SpringBoot自動配置登出實作
CasClientConfigurerImpl.java
package com.hellxz.cas;
import java.util.Map;
import org.jasig.cas.client.boot.configuration.CasClientConfigurer;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.stereotype.Component;
/**
* cas-client-support-springboot 依賴提供了CAS用戶端的自動配置,
* 當自動配置不滿足需要時,可通過實作{@link CasClientConfigurer}接口來重寫需要自定義的邏輯
*/
@Component
public class CasClientConfigurerImpl implements CasClientConfigurer {
/**
* 配置認證過濾器,添加忽略參數,使/logoutPage登出提示頁免登入
*/
@Override
@SuppressWarnings({ "rawtypes", "unchecked" })
public void configureAuthenticationFilter(final FilterRegistrationBean authenticationFilter) {
Map initParameters = authenticationFilter.getInitParameters();
initParameters.put("ignorePattern", "/logoutPage");
}
}
上邊這個配置類的作用是自定義認證過濾器,将 /logoutPage
排除不走認證邏輯,此頁面用于顯示登出提示。
CasAutoConfigApp.java
package com.hellxz.cas;
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.jasig.cas.client.boot.configuration.EnableCasClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@RestController
@EnableCasClient
public class CasAutoConfigApp {
@Value("${custom.cas.single-logout-url:}")
public String casSingleLogoutUrl;
public static void main(String[] args) {
SpringApplication.run(CasAutoConfigApp.class, args);
}
@GetMapping("/test")
public String test(HttpServletRequest request) {
return "服務A測試通過";
}
/**
* 首頁,需要登入
*/
@GetMapping("/index")
public String index(HttpServletRequest request) {
//@formatter:off
return "<h1>登入成功</h1><br><br>"
+ "<a href=\"/logout\">登出</a><br><br>"
+ "<a href=\"" + casSingleLogoutUrl + "\">全局登出</a>";
//@formatter:on
}
/**
* 登出提示頁,免登入
*/
@GetMapping("/logoutPage")
public String logoutPage(HttpServletResponse response) {
//@formatter:off
return "<h1>您已登出成功。</h1><br><br>"
+ "<a href=\"/index\">去登入</a><br><br>"
+ "<a href=\"" + casSingleLogoutUrl + "\">全局登出</a>";
//@formatter:on
}
/**
* 登出,跳轉登出提示頁
*/
@GetMapping("/logout")
public void logout(HttpServletRequest request, HttpServletResponse response) throws IOException {
HttpSession session = request.getSession(false);
if (session != null) {
// 過期會話
session.invalidate();
}
// 跳轉登出提示頁
response.sendRedirect("/logoutPage");
}
}
較上一疊代新增了(首頁)、
/index
(登出提示頁)、
/logoutPage
(用戶端登出)這三個接口。其中隻有
/logout
接口是免登入的,為了防止出現重定向回來自動登入的情況。
/logout
application.properties
這裡啟用了單點登出配置項,中的
CasClientConfiguration
與
casSingleSignOutFilter()
casSingleSignOutListener()
這兩個方法激活,注冊Bean到MVC容器中。
自定義單點登出位址相當于拼接 CAS服務端登出位址與回調重定向位址,這裡配置成免登入的用戶端位址
。
/logoutPage
手動配置登出實作
CasConfig.java,是上一疊代的Config.java重命名而來。
package com.hellxz.cas;
import java.util.EventListener;
import java.util.HashMap;
import java.util.Map;
import org.jasig.cas.client.authentication.AuthenticationFilter;
import org.jasig.cas.client.session.SingleSignOutFilter;
import org.jasig.cas.client.session.SingleSignOutHttpSessionListener;
import org.jasig.cas.client.util.HttpServletRequestWrapperFilter;
import org.jasig.cas.client.validation.Cas30ProxyReceivingTicketValidationFilter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
@Configuration
public class CasConfig {
/**
* 自定義cas服務位址
*/
@Value("${custom.cas.casServerUrlPrefix:}")
private String casServerUrlPrefix;
/**
* 自定義服務辨別,格式為{protocol}:{hostName}:{port}
*/
@Value("${custom.cas.serverName:}")
private String serverName;
/**
* 監聽登出事件,清除session與token之間的映射關系及CAS會話記錄
*/
@Bean
public ServletListenerRegistrationBean<EventListener> casSingleSignOutListener() {
ServletListenerRegistrationBean<EventListener> singleSignOutListener = new ServletListenerRegistrationBean<>();
singleSignOutListener.setListener(new SingleSignOutHttpSessionListener());
return singleSignOutListener;
}
@Bean
@Order(0)
public FilterRegistrationBean<SingleSignOutFilter> casSingleSignOutFilter() {
FilterRegistrationBean<SingleSignOutFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new SingleSignOutFilter());
registration.setName("CAS Single Sign Out Filter");
Map<String, String> initParams = new HashMap<>();
initParams.put("casServerUrlPrefix", casServerUrlPrefix); // CAS服務端位址,會拼接為登入位址
initParams.put("serverName", serverName); // 服務位址
registration.setInitParameters(initParams);
registration.addUrlPatterns("/*");
return registration;
}
/**
* 攔截所有請求,将未攜帶票據與會話中無票據的請求都重定向到CAS登入位址
*/
@Bean
@Order(1)
public FilterRegistrationBean<AuthenticationFilter> casAuthenticationFilter() {
FilterRegistrationBean<AuthenticationFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new AuthenticationFilter());
registration.setName("CAS Authentication Filter");
Map<String, String> initParams = new HashMap<>();
initParams.put("casServerUrlPrefix", casServerUrlPrefix); // CAS服務端位址,會拼接為登入位址
initParams.put("serverName", serverName); // 服務位址
// 自定義忽略認證的路徑或表達式,這裡用來免登入通路【登出提示】頁面
initParams.put("ignorePattern", "/logoutPage");
registration.setInitParameters(initParams);
registration.addUrlPatterns("/*");
return registration;
}
/**
* 攔截所有請求,使用擷取的票據向CAS服務端發起校驗票據請求
*/
@Bean
@Order(2)
public FilterRegistrationBean<Cas30ProxyReceivingTicketValidationFilter> cas30TicketValidationFilter() {
FilterRegistrationBean<Cas30ProxyReceivingTicketValidationFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new Cas30ProxyReceivingTicketValidationFilter());
registration.setName("CAS30 Ticket Validation Filter");
Map<String, String> initParams = new HashMap<>();
initParams.put("casServerUrlPrefix", casServerUrlPrefix); // CAS服務端位址,會拼接為服務校驗位址
initParams.put("serverName", serverName);
registration.setInitParameters(initParams);
registration.addUrlPatterns("/*");
return registration;
}
/**
* 包裝HttpServletRequest,使CAS登入成功的使用者名等資訊存入請求中<br>
* <br>
* 登入成功後以下兩個方法将不再傳回null: <br>
*
* <pre>
* HttpServletRequest#getUserPrincipal()
* HttpServletRequest#getRemoteUser()
* </pre>
*/
@Bean
@Order(3)
public FilterRegistrationBean<HttpServletRequestWrapperFilter> httpServletRequestWrapperFilter() {
FilterRegistrationBean<HttpServletRequestWrapperFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new HttpServletRequestWrapperFilter());
registration.setName("HttpServletRequest Wrapper Filter");
registration.addUrlPatterns("/*");
return registration;
}
}
新增了(配置單點登出監聽器)、
casSingleSignOutListener()
(單點登出過濾器)以及 将登出提示頁從認證過濾器處放行。
casSingleSignOutFilter()
需注意單點登出過濾器的排序要早于認證過濾器、校驗票據過濾器。// 自定義忽略認證的路徑或表達式,這裡用來免登入通路【登出提示】頁面 initParams.put("ignorePattern", "/logoutPage");
CasManualConfigApp.java
package com.hellxz.cas;
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@SpringBootApplication
public class CasManualConfigApp {
/**
* 自定義全局單點登出位址,由cas服務端位址/logout?service=目前serviceName/logoutPage組成<br>
* 當cas全局登出(帶TGC通路cas的/logout接口)成功後,會重定向service參數位址<br>
*
* <pre>
* 需注意:service參數必須含登入時注冊給CAS的serviceName,否則隻廢棄CAS會話而不會重定向
* </pre>
*/
@Value("${custom.cas.casSingleLogoutUrl:}")
private String casSingleLogoutUrl;
public static void main(String[] args) {
SpringApplication.run(CasManualConfigApp.class, args);
}
@GetMapping("/test")
public String test(HttpServletRequest request) {
return "服務B測試通過";
}
/**
* 首頁,需要登入
*/
@GetMapping("/index")
public String index(HttpServletRequest request) {
//@formatter:off
return "<h1>登入成功</h1><br><br>"
+ "<a href=\"/logout\">登出</a><br><br>"
+ "<a href=\"" + casSingleLogoutUrl + "\">全局登出</a>";
//@formatter:on
}
/**
* 登出提示頁,免登入
*/
@GetMapping("/logoutPage")
public String logoutPage(HttpServletResponse response) {
//@formatter:off
return "<h1>您已登出成功。</h1><br><br>"
+ "<a href=\"/index\">去登入</a><br><br>"
+ "<a href=\"" + casSingleLogoutUrl + "\">全局登出</a>";
//@formatter:on
}
/**
* 登出,跳轉登出提示頁
*/
@GetMapping("/logout")
public void logout(HttpServletRequest request, HttpServletResponse response) throws IOException {
HttpSession session = request.getSession(false);
if (session != null) {
// 過期會話
session.invalidate();
}
// 跳轉登出提示頁
response.sendRedirect("/logoutPage");
}
}
與 自動配置實作基本一緻,添加幾個接口供測試
驗證實作
以本地IP為 10.2.6.63,自動配置端口8081,手動配置端口 8082,CAS服務端192.168.56.104:8088/cas 為例
1、單點登入
啟動自動配置服務,通路本地端口号8081,我這裡是 http://10.2.6.63:8081/index ,通路首頁立即跳轉CAS登入頁面
輸入使用者名與密碼,casuser/Mellon,登入。如下圖單點登入成功
2、用戶端登出
先驗證退出用戶端登出,點
登出
。如圖進入登出成功提示頁面
檢察下 /logout 接口的 cookie值,此處是
95E21E8D67C363A7432C342EDACB4DE8
再點選
去登入
,這是通路 /index,可以看到又登入成功了,而且這次沒有手輸賬号密碼,檢視了cookie中的會話id為
6157E88046280B3E90A502016F98549A
,與退出之前不是同一會話
3、單點登出
接下來驗證CAS單點登出,點選
全局登出
。如下圖,可見通路CAS服務端 /logout接口,并傳遞了回調位址為登出提示頁面,最終回到了提示頁面。
我們再試驗一下
去登入
,驗證是否需要手輸賬号密碼登入。
如上圖,的确需要手輸賬号才能登入,說明單點登出功能正常。
4、CAS用戶端單點登出日志
由于自動配置項目我沒配置單點登出
trace
等級日志,我們用 手動配置服務登入再全局退出下,看看日志。
啟動手動配置服務,通路 http://10.2.6.63:8082/index,登入後再全局退出。日志如下:
2022-01-18 23:22:38.237 TRACE 24016 --- [nio-8082-exec-1] o.j.c.c.session.SingleSignOutHandler : Ignoring URI for logout: /index
2022-01-18 23:22:40.401 TRACE 24016 --- [nio-8082-exec-4] o.j.c.c.session.SingleSignOutHandler : Received a token request
2022-01-18 23:22:40.406 DEBUG 24016 --- [nio-8082-exec-4] o.j.c.c.session.SingleSignOutHandler : Recording session for token ST-61-aJ6BwiVkekqtkqlIXmmyWAYq6sMlocalhost
2022-01-18 23:22:40.535 TRACE 24016 --- [nio-8082-exec-3] o.j.c.c.session.SingleSignOutHandler : Ignoring URI for logout: /index
2022-01-18 23:22:43.698 TRACE 24016 --- [nio-8082-exec-5] o.j.c.c.session.SingleSignOutHandler : Received a logout request
2022-01-18 23:22:43.699 TRACE 24016 --- [nio-8082-exec-5] o.j.c.c.session.SingleSignOutHandler : Logout request:
<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="LR-37-W7V-UYdTLhGkwl7P2w2somtR" Version="2.0" IssueInstant="2022-01-18T10:22:43Z"><saml:NameID xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">@NOT_USED@</saml:NameID><samlp:SessionIndex>ST-61-aJ6BwiVkekqtkqlIXmmyWAYq6sMlocalhost</samlp:SessionIndex></samlp:LogoutRequest>
2022-01-18 23:22:43.702 DEBUG 24016 --- [nio-8082-exec-5] o.j.c.c.session.SingleSignOutHandler : Invalidating session [DB43DC21DCC1663D64968E8DBD48B247] for token [ST-61-aJ6BwiVkekqtkqlIXmmyWAYq6sMlocalhost]
2022-01-18 23:22:43.712 TRACE 24016 --- [nio-8082-exec-2] o.j.c.c.session.SingleSignOutHandler : Ignoring URI for logout: /logoutPage
可以看到:
- 登入成功的日志
Recording session for token ST-61-aJ6BwiVkekqtkqlIXmmyWAYq6sMlocalhost
- 單點登出的日志:
和Received a logout request
Logout request: <samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="LR-37-W7V-UYdTLhGkwl7P2w2somtR" Version="2.0" IssueInstant="2022-01-18T10:22:43Z"><saml:NameID xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">@NOT_USED@</saml:NameID><samlp:SessionIndex>ST-61-aJ6BwiVkekqtkqlIXmmyWAYq6sMlocalhost</samlp:SessionIndex></samlp:LogoutRequest>
- 過期目前用戶端會話的
Invalidating session [DB43DC21DCC1663D64968E8DBD48B247] for token [ST-61-aJ6BwiVkekqtkqlIXmmyWAYq6sMlocalhost]
至此驗證用戶端登出及單點登出功能一切正常。
自動配置和手動配置這兩個工程的效果是一樣的,筆者已經親身測試OK,就不在此重複表述了。
總結
本次編寫的 demo 恰如其分地驗證了CAS用戶端登出與單點登出的流程,即用戶端登出(過期自己)及單點登出(過期自己以及所有相關用戶端)。
參考:
- https://github.com/cas-projects/cas-sample-java-webapp/blob/master/src/main/webapp/WEB-INF/web.xml
- https://github.com/apereo/java-cas-client
本文同步于本人部落格園(hellxz.cnblogs.com) 與 CSDN(https://blog.csdn.net/u012586326),禁止轉載。