主要參考
粗略的解釋:
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); }
若是驗證經過, 會建立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的代碼
- //取得TGT_ID
- final String ticketGrantingTicketId = this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request);
- // 取得service參數數據,這個參數是可選參數
- final String service = request.getParameter("service");
-
- //若是TGT不爲空
- if (ticketGrantingTicketId != null) {
- //那麼在centralAuthenticationService中銷燬
- this.centralAuthenticationService
- .destroyTicketGrantingTicket(ticketGrantingTicketId);
- //ticketGrantingTicketCookieGenerator 中銷燬cookie
- this.ticketGrantingTicketCookieGenerator.removeCookie(response);
- //warnCookieGenerator 中銷燬
- this.warnCookieGenerator.removeCookie(response);
- }
- // 若是參數:followServiceRedirects爲true 同時service不會空的時候,跳轉到service指定的URL
- if (this.followServiceRedirects && service != null) {
- return new ModelAndView(new RedirectView(service));
- }
- //不然,跳轉到logoutView指定的頁面
- return new ModelAndView(this.logoutView);
- public void destroyTicketGrantingTicket(final String ticketGrantingTicketId) {
- //斷言參數不能空
- Assert.notNull(ticketGrantingTicketId);
-
- if (log.isDebugEnabled()) {
- log.debug("Removing ticket [" + ticketGrantingTicketId + "] from registry.");
- }
- // 從票據倉庫中取得TGT票據
- final TicketGrantingTicket ticket = (TicketGrantingTicket) this.ticketRegistry.getTicket(ticketGrantingTicketId, TicketGrantingTicket.class);
- //若是票據爲空,則直接返回
- if (ticket == null) {
- return;
- }
-
- if (log.isDebugEnabled()) {
- log.debug("Ticket found. Expiring and then deleting.");
- }
- //叫票據註銷,也就是設置爲期滿(或者叫作過時)
- ticket.expire();
- //在票據倉庫中刪除該票據
- this.ticketRegistry.deleteTicket(ticketGrantingTicketId);
- }
public synchronized void
expire
() {
this.expired = true;
logOutOfServices
();
}
- private void logOutOfServices() {
- for (final Entry<String, Service> entry : this.services.entrySet()) {
-
- if (!entry.getValue().logOutOfService(entry.getKey())) {
- LOG.warn("Logout message not sent to [" + entry.getValue().getId() + "]; Continuing processing...");
- }
- }
- }
原來在TGT票據裏面有個Entry來保存用戶訪問過的service對象,因此,這裏的services的列表,會循環這個列表 給 CLIENT發送註銷請求的.
key是對應service的
seesionID ,因此,這裏每一個用戶的jsessionid,就有做用了.
下面是發送請求的代碼
- public synchronized boolean logOutOfService(final String sessionIdentifier) {
- if (this.loggedOutAlready) {
- return true;
- }
-
- LOG.debug("Sending logout request for: " + getId());
-
- final String logoutRequest = "<samlp:LogoutRequest xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" ID=\""
- + GENERATOR.getNewTicketId("LR")
- + "\" Version=\"2.0\" IssueInstant=\"" + SamlUtils.getCurrentDateAndTime()
- + "\"><saml:NameID xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">@NOT_USED@</saml:NameID><samlp:SessionIndex>"
- + sessionIdentifier + "</samlp:SessionIndex></samlp:LogoutRequest>";
-
- this.loggedOutAlready = true;
-
- if (this.httpClient != null) {
- return this.httpClient.sendMessageToEndPoint(getOriginalUrl(), logoutRequest, true);
- }
-
- return false;
- }
客戶端須要在 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 裏移除.
這樣子,應該全部的子系統都會登出了.