CAS學習筆記五:SpringBoot自動/手動配置方式整合CAS單點登出

東北小狐狸發表於2022-01-19

本文目標

基於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");
    }
}

與 自動配置實現基本一致,新增幾個介面供測試

application.properties

驗證實現

以本地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 requestLogout 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客戶端登出與單點登出的流程,即客戶端登出(過期自己)及單點登出(過期自己以及所有相關客戶端)。

參考:

本文同步於本人部落格園(hellxz.cnblogs.com) 與 CSDN(https://blog.csdn.net/u012586326),禁止轉載。

相關文章