主要参考
粗略的解释:
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 里移除.
这样子,应该全部的子系统都会登出了.