CAS小總結

主要參考
粗略的解釋:
1. 客戶登陸 www.xn.com/1.html,(假設全部的資源,都會被filter攔截,這裏會引起一些思考, 若是靜態資源不攔截, AJAX的動態請求,若是在沒有session的狀況下, 會被跨域重定向到https://login.xn.com/login上,確定會返回response空的狀況,會給前端帶來問題), 首先進入 www.xn.com應用的 AuthenticationFilter > doFilter ,從代碼裏看,若是 Assertion 是session裏的一個驗證明體, 在登陸成功後,會被設置到session裏.
public final void doFilter( final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException { final HttpServletRequest request = (HttpServletRequest) servletRequest; final HttpServletResponse response = (HttpServletResponse) servletResponse; final HttpSession session = request.getSession( false ); // 該變量爲判斷用戶是否已經登陸的標記,在用戶成功登陸後會被設置 final Assertion assertion = session != null ? (Assertion) session.getAttribute(CONST_CAS_ASSERTION) : null ; // 判斷是否登陸過,若是已經登陸過,進入if而且退出 if (assertion != null ) { filterChain.doFilter(request, response); return ; } // 若是沒有登陸過,繼續後續處理 // 構造訪問的URL,若是該Url包含tikicet參數,則去除參數 final String serviceUrl = constructServiceUrl(request, response); // 若是ticket存在,則獲取URL後面的參數ticket final String ticket = CommonUtils.safeGetParameter(request, getArtifactParameterName()); // 研究中 final boolean wasGatewayed = this .gatewayStorage.hasGatewayedAlready(request, serviceUrl); // 若是ticket存在 if (CommonUtils.isNotBlank(ticket) || wasGatewayed) { filterChain.doFilter(request, response); return ; } final String modifiedServiceUrl; log.debug("no ticket and no assertion found"); if ( this .gateway) { log.debug("setting gateway attribute in session"); modifiedServiceUrl = this .gatewayStorage.storeGatewayInformation(request, serviceUrl); } else { modifiedServiceUrl = serviceUrl; } if (log.isDebugEnabled()) { log.debug("Constructed service url: " + modifiedServiceUrl); } // 若是用戶沒有登陸過,那麼構造重定向的URL final String urlToRedirectTo = CommonUtils.constructRedirectUrl( this .casServerLoginUrl, getServiceParameterName(), modifiedServiceUrl, this .renew, this .gateway); if (log.isDebugEnabled()) { log.debug("redirecting to \"" + urlToRedirectTo + "\""); } // 重定向跳轉到Cas認證中心 response.sendRedirect(urlToRedirectTo); }
大概意思,就是若是確認沒有登陸(url中沒有ticket,而且 session也沒有或者session裏沒有驗證明體)就會被重定向到 https://login.xn.com/login?service=http://www.xn.com/1.html
2. 瀏覽器收到重定向的response,就會訪問 https://login.xn.com/login?service=http://www.xn.com/1.html , CAS SERVER會 從 login.xn.com域的 cookie 檢查是否有 CASTGC,並獲取一下service裏的URL,會發現沒有 CASTGC,因此,是個沒登陸的用戶,就會出現登陸頁,用戶會輸入並驗證.
若是驗證經過, 會建立TGT(就是TGC對應在 CAS SERVER 上的實體,這裏,應該會有TGT<->Service的對應),而且把 TGC寫入到 Cookie(login.xn.com域下),而且由於有 service的存在,(這裏service主要是用來告訴CAS SERVER,你得告訴瀏覽器,一下子重定向到源URL上才行),因此會生成 ST (service ticket),並告訴瀏覽器,重定向到 www.xn.com/1.html?ticket=st123456上.
3. 瀏覽器會根據重定向的地址 www.xn.com/1.html?ticket=st123456上請求WEB服務器,因此,會繼續進入 AuthenticationFilter ,可是此次由於有了ticket,根據源碼,會進入下一個filter Cas20ProxyReceivingTicketValidationFilter , 該攔截器用來與 CAS Server 進行身份覈實,以確保 Service Ticket 的合法性.
// 構造驗證URL,向cas server發起驗證請求 final Assertion assertion = this . ticketValidator.validate(ticket, constructServiceUrl(request, response)); if (log.isDebugEnabled()) { log.debug("Successfully authenticated user: " + assertion.getPrincipal().getName()); } // 若是驗證成功,設置assertion,當再一次發起訪問請求時,若是發現assertion已經被設置,因此已經經過驗證,不過再次重定向會cas認證中心 request.setAttribute(CONST_CAS_ASSERTION, assertion); if ( this .useSession) { request.getSession().setAttribute(CONST_CAS_ASSERTION, assertion); }
這裏會發現,若是從CAS SERVER上驗證ST沒問題,就應該是登陸成功了,會設置 assertion到Session中. 這裏寫了,會構造URL,感受應該相似於 http client針對 cas server發起了一次請求,而後返回 assertion.
// 生成驗證URL,若是你debug會發現,此處會構造一個相似如下的URL,訪問的是cas server的serviceValidate方法 ,示例以下 // https://demo.testcas.com/cas/ serviceValidate ? ticket= ST-31-cioaDNxSpUWIgeYEn4yK-cas & service= http%3A%2F%2Fapp1.testcas.com%2Fb2c-haohai-server%2Fuser%2FcasLogin
在 CAS SEVER那裏, 會觸發 final Assertion assertion = this.centralAuthenticationService.validateServiceTicket(serviceTicketId, service);
這個函數, 因此,是感受server與 ST的對應關係,來檢查ST是否有效
若是發現沒問題了,就會通知瀏覽器,重定向 到 www.xn.com/1.html上,就能夠了,這是最後一次重定向(這裏的assertion感受應該也會保存用戶的一些信息,否則不可能每次client getuser的時候,都要連接CAS SERVER).
3. 當瀏覽器收到重定向通知的時候, 就開始訪問 源URL www.xn.com/1.html, 一樣會觸發 filter,這是,第一個filter會檢查,發現已經登陸了,而後交給ticketfilter, 發現沒有 ticket,繼續沿着 chain往下(剩下的應該不重要,會繼續觸發其餘的HTTP行爲了),這就是第一登陸的完整過程了.
當用戶繼續新標籤打開www.xn.net的其餘連接,都會被authenticationfilter攔截, session存在,而且有 assertion,因此,會是正常的.
若是這個時候,關閉瀏覽器 jsession會失效,因此,再打開瀏覽器的時候,訪問又有變化了
由於session裏沒有 assertion了(jsession若是變了,確定找不到用戶上次回話對應的session了),因此此次訪問,authentication會檢查到session失效了,因而,又開始重定向到 https://login.xn.com/login?service=***上,可是 由於 login.xn.com的cookie的存在(裏面有TGC),因此CAS SERVER會根據TGC知道,這個用戶登陸過,因而就能夠不用登陸了,直接生成 ST,而後告訴瀏覽器重定向到 service對應的RUL+?ticket=st333333,繼續上面的驗證了.
(這裏有一點,登陸成功後 ,jsession id 跟 service 的對應關係,也會保留在CAS SERVER的,這個應該是註銷的時候,會用到.)
當用戶訪問 cs.xn.com/productCenter/index.html的時候,由於配置的CAS CLIENT,因此,會依然被重定向到 https://login.xn.com/login?service=cs.xn.com/productCenter/index.html ,這時,跟上面類同,也會先查TGC,發現登陸過,直接返回 st,而後繼續校驗走一遍,就能夠經過 assertion獲取 user了,無須登陸.
這裏項目出現過一個問題, 項目首頁是個靜態頁面,裏面有ajax異步請求JAVA的後臺服務,因此,若是session莫名失效, ajax請求的時候,會被 filter重定向,由於跨域的關係,請求確定會失敗的.
若是隻是單單解決這個問題,能夠隨便在ajax執行前,有其餘沒有被排除的靜態資源訪問,先觸發filter的話,應該能夠避免ajax跨域重定向的問題.(好比,能夠加載一個 不存在的img(這樣不會被緩存,這個404的img會直接請求WEB服務的,或者在第一個 <script>裏,var img = new img("src"),也會使AJAX肯定不會先執行).
登陸過程是這個樣子的, 登出過程沒有詳細的瞭解,只是簡單說一下從網上看來的解釋.
好比 www.xn.com 有註銷功能, 會去請求 https://login.xn.com/logout(後面有沒有service我也不知道), CAS SERVER會檢查 login.xn.com域裏的 Cookie,裏面有TGC,因此能夠找到不少信息.
CAS SERVER的代碼
  1. //取得TGT_ID  
  2.      final String ticketGrantingTicketId = this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request);  
  3. // 取得service參數數據,這個參數是可選參數  
  4.      final String service = request.getParameter("service");  
  5.        
  6.      //若是TGT不爲空  
  7.      if (ticketGrantingTicketId != null) {  
  8.         //那麼在centralAuthenticationService中銷燬  
  9.          this.centralAuthenticationService  
  10.              .destroyTicketGrantingTicket(ticketGrantingTicketId);  
  11.          //ticketGrantingTicketCookieGenerator 中銷燬cookie  
  12.          this.ticketGrantingTicketCookieGenerator.removeCookie(response);  
  13.          //warnCookieGenerator 中銷燬  
  14.          this.warnCookieGenerator.removeCookie(response);  
  15.      }  
  16.      // 若是參數:followServiceRedirects爲true 同時service不會空的時候,跳轉到service指定的URL  
  17.      if (this.followServiceRedirects && service != null) {  
  18.          return new ModelAndView(new RedirectView(service));  
  19.      }  
  20.      //不然,跳轉到logoutView指定的頁面  
  21.      return new ModelAndView(this.logoutView);  
  1. public void destroyTicketGrantingTicket(final String ticketGrantingTicketId) {  
  2.         //斷言參數不能空  
  3.         Assert.notNull(ticketGrantingTicketId);  
  4.           
  5.         if (log.isDebugEnabled()) {  
  6.             log.debug("Removing ticket [" + ticketGrantingTicketId + "] from registry.");  
  7.         }  
  8.         // 從票據倉庫中取得TGT票據  
  9.         final TicketGrantingTicket ticket = (TicketGrantingTicket) this.ticketRegistry.getTicket(ticketGrantingTicketId, TicketGrantingTicket.class);  
  10.         //若是票據爲空,則直接返回  
  11.         if (ticket == null) {  
  12.             return;  
  13.         }  
  14.   
  15.         if (log.isDebugEnabled()) {  
  16.             log.debug("Ticket found.  Expiring and then deleting.");  
  17.         }  
  18.         //叫票據註銷,也就是設置爲期滿(或者叫作過時)  
  19.         ticket.expire();  
  20.         //在票據倉庫中刪除該票據  
  21.         this.ticketRegistry.deleteTicket(ticketGrantingTicketId);  
  22.     }  
public synchronized void expire () {
        this.expired = true;
        logOutOfServices ();
    }
  1. private void logOutOfServices() {  
  2.        for (final Entry<String, Service> entry : this.services.entrySet()) {  
  3.   
  4.            if (!entry.getValue().logOutOfService(entry.getKey())) {  
  5.                LOG.warn("Logout message not sent to [" + entry.getValue().getId() + "]; Continuing processing...");     
  6.            }  
  7.        }  
  8.    }  
原來在TGT票據裏面有個Entry來保存用戶訪問過的service對象,因此,這裏的services的列表,會循環這個列表 給 CLIENT發送註銷請求的. key是對應service的 seesionID ,因此,這裏每一個用戶的jsessionid,就有做用了.
下面是發送請求的代碼
  1. public synchronized boolean logOutOfService(final String sessionIdentifier) {  
  2.        if (this.loggedOutAlready) {  
  3.            return true;  
  4.        }  
  5.   
  6.        LOG.debug("Sending logout request for: " + getId());  
  7.   
  8.        final String logoutRequest = "<samlp:LogoutRequest xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" ID=\""  
  9.            + GENERATOR.getNewTicketId("LR")  
  10.            + "\" Version=\"2.0\" IssueInstant=\"" + SamlUtils.getCurrentDateAndTime()  
  11.            + "\"><saml:NameID xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">@NOT_USED@</saml:NameID><samlp:SessionIndex>"  
  12.            + sessionIdentifier + "</samlp:SessionIndex></samlp:LogoutRequest>";  
  13.          
  14.        this.loggedOutAlready = true;  
  15.          
  16.        if (this.httpClient != null) {  
  17.            return this.httpClient.sendMessageToEndPoint(getOriginalUrl(), logoutRequest, true);  
  18.        }  
  19.          
  20.        return false;  
  21.    }  
客戶端須要在 authencationfilter前加上一個 logout的filter和 listener
< filter >
    < filter-name > CAS Single Sign Out Filter </ filter-name >
    < filter-class > org.jasig.cas.client.session.SingleSignOutFilter </ filter-class >
</ filter >
< filter-mapping >
    < filter-name > CAS Single Sign Out Filter </ filter-name >
    < url-pattern > /* </ url-pattern >
</ filter-mapping >
< listener >
     < listener-class > org.jasig.cas.client.session.SingleSignOutHttpSessionListener </ listener-class >
</ listener >
SingleSignOutFilter:
public  void  doFilter( final  ServletRequest servletRequest,  final  ServletResponse servletResponse,  final  FilterChain      
 2 
 3  filterChain)  throws  IOException, ServletException {
 4           final  HttpServletRequest request = (HttpServletRequest) servletRequest;
 5 
 6           if  ("POST".equals(request.getMethod())) {
 7               final  String logoutRequest = request.getParameter("logoutRequest");
 8 
 9               if  (CommonUtils.isNotBlank(logoutRequest)) {
10 
11                   if  (log.isTraceEnabled()) {
12                      log.trace ("Logout request=[" + logoutRequest + "]");
13                  }
14                   //從xml中解析 SessionIndex key值
15                   final  String sessionIdentifier = XmlUtils.getTextForElement(logoutRequest, "SessionIndex");
16 
17                   if  (CommonUtils.isNotBlank(sessionIdentifier)) {
18                           //根據sessionId取得session對象
19                       final  HttpSession session = SESSION_MAPPING_STORAGE.removeSessionByMappingId(sessionIdentifier);
20 
21                       if  (session !=  null ) {
22                          String sessionID = session.getId();
23 
24                           if  (log.isDebugEnabled()) {
25                              log.debug ("Invalidating session [" + sessionID + "] for ST [" + sessionIdentifier + "]");
26                          }
27                          
28                           try  {
29                   //讓session失效
30                              session.invalidate();
31                          }  catch  ( final  IllegalStateException e) {
32                              log.debug(e,e);
33                          }
34                      }
35                     return ;
36                  }
37              }
38          }  else  { //get方式 表示登陸,把session對象放到SESSION_MAPPING_STORAGE(map對象中)
39               final  String artifact = request.getParameter( this .artifactParameterName);
40               final  HttpSession session = request.getSession();
41              
42               if  (log.isDebugEnabled() && session !=  null ) {
43                  log.debug("Storing session identifier for " + session.getId());
44              }
45               if  (CommonUtils.isNotBlank(artifact)) {
46                  SESSION_MAPPING_STORAGE.addSessionById(artifact, session);
47              }
48          }
49 
50          filterChain.doFilter(servletRequest, servletResponse);
51      }
先無論別的, 這麼看,應該是 CAS SERVER 的 HTTPCLIENT 發起了一個相似
http://cs.xn.com/***?logoutRequest*** 大概這樣子的請求, 裏面有 session id(由於 server是遍歷的形式,因此,即便有些CLIENT這邊失效的sessionid,也會發過來)而後從 SingleSignOutHttpSessionListener 持有的全局 session表中得到 session實例,而後 session.invalidate();失效.而且從 全局 SESSION_MAPPING_STORAGE 裏移除.
這樣子,應該全部的子系統都會登出了.
相關文章
相關標籤/搜索