CAS單點登錄源碼解析之【客戶端】


cas 3.5.3服務器搭建+spring boot集成+shiro模擬登錄(不修改現有shiro認證架構)。因為我們屬於供應商,所以有些客戶那里會需要接對方的CAS,所以沒有使用shiro和cas的直接集成模式,如果是這種情況,可以參考:https://blog.csdn.net/catoop/article/details/50534006。

Cas Client主要有四個核心過濾器:

l  AuthenticationFilter

l  TicketValidationFilter

l  HttpServletRequestWrapperFilter

l  AssertionThreadLocalFilter

他們的作用解釋如下:

AuthenticationFilter

AuthenticationFilter用來攔截所有的請求,用以判斷用戶是否需要通過Cas Server進行認證,如果需要則將跳轉到Cas Server的登錄頁面。如果不需要進行登錄認證,則請求會繼續往下執行。

     AuthenticationFilter有兩個用戶必須指定的參數,一個是用來指定Cas Server登錄地址的casServerLoginUrl,另一個是用來指定認證成功后需要跳轉地址的serverNameservice。service和serverName只需要指定一個就可以了。當兩者都指定了,參數service將具有更高的優先級,即將以service指定的參數值為准。service和serverName的區別在於service指定的是一個確定的URL,認證成功后就會確切的跳轉到service指定的URL;而serverName則是用來指定主機名,其格式為{protocol}:{hostName}:{port},如:https://localhost:8443,當指定的是serverName時,AuthenticationFilter將會把它附加上當前請求的URI,以及對應的查詢參數來構造一個確定的URL,如指定serverName為“http://localhost”,而當前請求的URI為“/app”,查詢參數為“a=b&b=c”,則對應認證成功后的跳轉地址將為“http://localhost/app?a=b&b=c”。

除了上述必須指定的參數外,AuthenticationFilter還可以指定如下可選參數:

l  renew:當指定renew為true時,在請Cas Server時將帶上參數“renew=true”,默認為false。

l  gateway:指定gateway為true時,在請求Cas Server時將帶上參數“gateway=true”,默認為false。

l  artifactParameterName:指定ticket對應的請求參數名稱,默認為ticket。

l  serviceParameterName:指定service對應的請求參數名稱,默認為service。

 

TicketValidationFilter

在請求通過AuthenticationFilter的認證之后,如果請求中攜帶了參數ticket則將會由TicketValidationFilter來對攜帶的ticket進行校驗。TicketValidationFilter只是對驗證ticket的這一類Filter的統稱,其並不對應Cas Client中的一個具體類型。Cas Client中有多種驗證ticket的Filter,都繼承自AbstractTicketValidationFilter,它們的驗證邏輯都是一致的,都有AbstractTicketValidationFilter實現,所不同的是使用的TicketValidator不一樣。默認是Cas10TicketValidationFilter。

可選參數包括:

l  redirectAfterValidation :表示是否驗證通過后重新跳轉到該URL,但是不帶參數ticket,默認為true。

l  useSession :在驗證ticket成功后會生成一個Assertion對象,如果useSession為true,則會將該對象存放到Session中。如果為false,則要求每次請求都需要攜帶ticket進行驗證,顯然useSession為false跟redirectAfterValidation為true是沖突的。默認為true。

l  exceptionOnValidationFailure :表示ticket驗證失敗后是否需要拋出異常,默認為true。

l  renew:當值為true時將發送“renew=true”到Cas Server,默認為false。

 

HttpServletRequestWrapperFilter

HttpServletRequestWrapperFilter用於將每一個請求對應的HttpServletRequest封裝為其內部定義的CasHttpServletRequestWrapper,該封裝類將利用之前保存在Session或request中的Assertion對象重寫HttpServletRequest的getUserPrincipal()、getRemoteUser()和isUserInRole()方法。這樣在我們的應用中就可以非常方便的從HttpServletRequest中獲取到用戶的相關信息。

 

AssertionThreadLocalFilter

AssertionThreadLocalFilter是為了方便用戶在應用的其它地方獲取Assertion對象,其會將當前的Assertion對象存放到當前的線程變量中,那么以后用戶在程序的任何地方都可以從線程變量中獲取當前Assertion,無需再從Session或request中進行解析。該線程變量是由AssertionHolder持有的,我們在獲取當前的Assertion時也只需要通過AssertionHolder的getAssertion()方法獲取即可,如:

   Assertion assertion = AssertionHolder.getAssertion();

 

cas client/shiro過濾器順序,集成cas的情況下,cas客戶端總是第一個過濾器。
org.jasig.cas.client.validation.AbstractTicketValidationFilter#doFilter
    org.jasig.cas.client.util.CommonUtils#safeGetParameter
org.jasig.cas.client.authentication.AuthenticationFilter#doFilter --在這里修改源碼,拿到assertion、且為空后需要判斷token是否存在且有效,如果存在且有效,說明已經登錄過,則直接返回
    org.jasig.cas.client.util.CommonUtils#constructRedirectUrl 
    javax.servlet.http.HttpServletResponse#sendRedirect --此時請求去了CAS

最后進入shiro過濾器UserFilter.isAccessAllowed

 

因為在userfilter里面標准的cas登錄后,是可以通過UserPrincipal拿到當前的用戶信息的,但是當我們是集群模式的時候因為直接在AuthenticationFilter#doFilter中攔截返回了,且沒有明確設置,自然就拿不到了,servletrequest中沒有提供設置UserPrincipal的入口,cas的org.jasig.cas.client.validation.Cas20ServiceTicketValidator#parseResponseFromServer是創建了,但是org.jasig.cas.client.validation.Cas20ServiceTicketValidator#customParseResponse的實現體是空的。如下:

protected final Assertion parseResponseFromServer(final String response) throws TicketValidationException {
        final String error = XmlUtils.getTextForElement(response,
                "authenticationFailure");

        if (CommonUtils.isNotBlank(error)) {
            throw new TicketValidationException(error);
        }

        final String principal = XmlUtils.getTextForElement(response, "user");
        final String proxyGrantingTicketIou = XmlUtils.getTextForElement(response, "proxyGrantingTicket");
        final String proxyGrantingTicket = this.proxyGrantingTicketStorage != null ? this.proxyGrantingTicketStorage.retrieve(proxyGrantingTicketIou) : null;

        if (CommonUtils.isEmpty(principal)) {
            throw new TicketValidationException("No principal was found in the response from the CAS server.");
        }

        final Assertion assertion;
        final Map<String,Object> attributes = extractCustomAttributes(response);
        if (CommonUtils.isNotBlank(proxyGrantingTicket)) {
            final AttributePrincipal attributePrincipal = new AttributePrincipalImpl(principal, attributes, proxyGrantingTicket, this.proxyRetriever);
            assertion = new AssertionImpl(attributePrincipal);
        } else {
            assertion = new AssertionImpl(new AttributePrincipalImpl(principal, attributes));
        }

        customParseResponse(response, assertion);

        return assertion;
    }

stackoverflow也沒搜到。如下:

https://stackoverflow.com/questions/25885747/when-and-where-java-security-set-userprincipal

https://docs.oracle.com/javase/7/docs/jre/api/security/jaas/spec/com/sun/security/auth/UserPrincipal.html

此時如果設置UserPrincipal需要的話,需要自己通過HttpServletRequestWrapperFilter重寫一遍,如下:

    public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException {
        final AttributePrincipal principal = retrievePrincipalFromSessionOrRequest(servletRequest);

        filterChain.doFilter(new CasHttpServletRequestWrapper((HttpServletRequest) servletRequest, principal), servletResponse);
    }

    protected AttributePrincipal retrievePrincipalFromSessionOrRequest(final ServletRequest servletRequest) {
        final HttpServletRequest request = (HttpServletRequest) servletRequest;
        final HttpSession session = request.getSession(false);
        final Assertion assertion = (Assertion) (session == null ? request.getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION) : session.getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION));

        return assertion == null ? null : assertion.getPrincipal();
    }

這樣的話,shiro UserFilter里面就可以通過

AttributePrincipal principal = (AttributePrincipal)((HttpServletRequest) request).getUserPrincipal();

拿到登錄用戶的信息了。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM