2019独角兽企业重金招聘Python工程师标准>>>

我们在项目中使用了cas作为单点登录的解决方案,当在集成shiro做统一权限控制的时候,发现单点退出登录有坑,所以啃了一下CAS的单点登出的源码,在此分享一下。

1、回顾单点登录中一些关键事件

在解析CAS单点登出的原理之前,我们先回顾一下在单点登录过程中,CAS服务器和CAS客户端都做了一些什么事,这些事在后面解析单点登出时有助于理解。

一般情况下,在项目中使用cas client提供的几个过滤器实现WEB APP的单点登录、退出功能,配置如下:

<listener><listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class></listener><filter><filter-name>CAS Single Sign Out Filter</filter-name><filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class><init-param><param-name>casServerUrlPrefix</param-name><param-value>http://passport.edu:18080</param-value></init-param></filter><filter-mapping><filter-name>CAS Single Sign Out Filter</filter-name><url-pattern>/*</url-pattern></filter-mapping><filter><filter-name>CAS Authentication Filter</filter-name><filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class><init-param><param-name>casServerLoginUrl</param-name><param-value>http://passport.edu:18080/login</param-value></init-param><init-param><param-name>serverName</param-name><param-value>http://jd.edu:9443</param-value></init-param></filter><filter-mapping><filter-name>CAS Authentication Filter</filter-name><url-pattern>/groupon/*</url-pattern></filter-mapping><filter><filter-name>CAS Validation Filter</filter-name><filter-class>org.jasig.cas.client.validation.Cas30ProxyReceivingTicketValidationFilter</filter-class><init-param><param-name>casServerUrlPrefix</param-name><param-value>http://passport.edu:18080</param-value></init-param><init-param><param-name>serverName</param-name><param-value>http://jd.edu:9443</param-value></init-param><init-param><param-name>redirectAfterValidation</param-name><param-value>true</param-value></init-param></filter><filter-mapping><filter-name>CAS Validation Filter</filter-name><url-pattern>/*</url-pattern></filter-mapping><filter><filter-name>CAS HttpServletRequest Wrapper Filter</filter-name><filter-class>org.jasig.cas.client.util.HttpServletRequestWrapperFilter</filter-class></filter><filter-mapping><filter-name>CAS HttpServletRequest Wrapper Filter</filter-name><url-pattern>/*</url-pattern></filter-mapping>

(1)CAS服务器在用户填入表单登录成功后,会在用户浏览器的cas 服务器所在域的cookie中存入TGC,即ticket granting cookie,它是加密的,里面包含TGT的id,以及浏览器的信息。

清单:TGC未加密前的信息

TGT-**********************************************aPD6RZNcJg-passport.edu@127.0.0.1@Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36]

清单:TGC加密后的信息

另外,CAS服务器内部会创建一个缓存存放TGT对象。TGT对象的ID就是TGC的ID,它还保存了一个非常重要的一个map:services

services ,这个名词是不是很熟悉?我们的应用服务器APP对于CAS服务器就是一个service。在cas server的配置文件中可以限定哪些service可以访问CAS服务器,另外,在我们的重定向到CAS登录的URL中,也必须告诉CAS当前访问它的service是谁。扯远了,解释一下,当web app应用系统获得登录认证后,需要在CAS上注册它已经被授权登录了,这时应用服务器将获取被授权登录的票据ST(service ticket),CAS服务器为应用服务器创建了Service对象用于保存它的一些信息(最重要的就是ID和认证信息了),并把service保存到services这个map中,该map的key就是ST了。

(2)CAS客户端在SingleSignOutFilter过滤器中,获取CAS服务器返回Service Ticket,将为ST与session建立映射关系,该映射关系将会在单点登出的时候使用。

具体的登录流程,请参考《单点登录CAS登录流程》

2、单点登出的原理

整个注销流程大致可以分为TGT解码和ticket销毁两个步骤。

2.1 TGT解码

整个注销流程起源于浏览器向CAS服务器发起登出请求:http://passport.edu:18080/logout?service=http://jd.edu:9443。

CAS服务接收请求后,获取浏览器的cookie中的tgc信息,对tgc信息进行解密,解密后将获取到tgt的ID,然后由CentralAuthenticationServiceImpl 类的 destroyTicketGrantingTicket()方法注销该TGT。

2.2 ticket销毁

由于CAS服务器和应用服务器都保存了ticket,所以CAS服务器除了自己销毁ticket外,还需要通知应用服务器销毁ticket。下面我们看一下详细流程。

=========+=======我是分割线,下面是CAS服务器端分析=======================

看一下 CentralAuthenticationServiceImpl 类的 destroyTicketGrantingTicket()方法。

public List<LogoutRequest> destroyTicketGrantingTicket(@NotNull final String ticketGrantingTicketId) {try {// 根据tgt ID从ticketRegistry注册中心中获取TGTfinal TicketGrantingTicket ticket = getTicket(ticketGrantingTicketId, TicketGrantingTicket.class);// 备注(1):由LogoutManager 完成注销 final List<LogoutRequest> logoutRequests = logoutManager.performLogout(ticket);// 备注(2):注册中心删除该tgtthis.ticketRegistry.deleteTicket(ticketGrantingTicketId);return logoutRequests;} catch (final InvalidTicketException e) {logger.debug("TicketGrantingTicket [{}] cannot be found in the ticket registry.", ticketGrantingTicketId);}return Collections.emptyList();
}

代码中的备注(1)完成客户端的ticket销毁,备注(2)完成CAS服务器的ticket销毁。备注(1)的登出管理器的实现类是 LogoutManagerImpl,看一下它的performLogout方法。

@Override
public List<LogoutRequest> performLogout(final TicketGrantingTicket ticket) {final Map<String, Service> services = ticket.getServices(); // 获取注册在tgt下的servicefinal List<LogoutRequest> logoutRequests = new ArrayList<>();if (!this.singleLogoutCallbacksDisabled) {// 遍历所有的servicefor (final Map.Entry<String, Service> entry : services.entrySet()) {// it's a SingleLogoutService, else ignorefinal Service service = entry.getValue();if (service instanceof SingleLogoutService) {// 对service进行登出操作final LogoutRequest logoutRequest = handleLogoutForSloService((SingleLogoutService) service, entry.getKey());if (logoutRequest != null) {LOGGER.debug("Captured logout request [{}]", logoutRequest);logoutRequests.add(logoutRequest);}}}}

继续看一下handleLogoutForSloService方法

private LogoutRequest handleLogoutForSloService(final SingleLogoutService singleLogoutService, final String ticketId) {if (!singleLogoutService.isLoggedOutAlready()) {// 备注(1):从服务管理器中获取匹配的已注册的服务final RegisteredService registeredService = servicesManager.findServiceBy(singleLogoutService);if (serviceSupportsSingleLogout(registeredService)) {// 决定使用哪个登出URL,如果registeredService指定了就用它的,不然就用singleLogoutService里的URL// 一般registeredService不会指定final URL logoutUrl = determineLogoutUrl(registeredService, singleLogoutService);// 包装登出请求final DefaultLogoutRequest logoutRequest = new DefaultLogoutRequest(ticketId, singleLogoutService, logoutUrl);final LogoutType type = registeredService.getLogoutType() == null? LogoutType.BACK_CHANNEL : registeredService.getLogoutType();switch (type) {case BACK_CHANNEL:// 通知应用服务器注销ticketif (performBackChannelLogout(logoutRequest)) {logoutRequest.setStatus(LogoutRequestStatus.SUCCESS);} else {logoutRequest.setStatus(LogoutRequestStatus.FAILURE);LOGGER.warn("Logout message not sent to [{}]; Continuing processing...", singleLogoutService.getId());}break;default:logoutRequest.setStatus(LogoutRequestStatus.NOT_ATTEMPTED);break;}return logoutRequest;}}return null;
}

备注(1)中,servicesManager.findServiceBy( ) 该方法将会遍历在servicesManager注册的服务,并且查看service是否匹配RegisteredService。RegisteredService是什么呢?

RegisteredService是在cas初始化中,加载配置文件后注册在服务管理器中的服务信息,该信息定义了哪些应用服务器可以接入CAS,登出的类型是什么。

大家是否还记得在CAS服务器的搭建时,是不是修改过 HTTPSandIMAPS-10000001.json 的serviceID呢?这个配置文件就是定义了一个RegisteredService。

清单:HTTPSandIMAPS-10000001.json

{"@class" : "org.jasig.cas.services.RegexRegisteredService","serviceId" : "^(https|imaps|http)://.*","name" : "HTTPS and IMAPS","id" : 10000001,"description" : "This service definition authorized all application urls that support HTTPS and IMAPS protocols.","proxyPolicy" : {"@class" : "org.jasig.cas.services.RefuseRegisteredServiceProxyPolicy"},"evaluationOrder" : 0,"usernameAttributeProvider" : {"@class" : "org.jasig.cas.services.DefaultRegisteredServiceUsernameProvider"},"logoutType" : "BACK_CHANNEL","attributeReleasePolicy" : {"@class" : "org.jasig.cas.services.ReturnAllowedAttributeReleasePolicy","principalAttributesRepository" : {"@class" : "org.jasig.cas.authentication.principal.DefaultPrincipalAttributesRepository"},"authorizedToReleaseCredentialPassword" : false,"authorizedToReleaseProxyGrantingTicket" : false},"accessStrategy" : {"@class" : "org.jasig.cas.services.DefaultRegisteredServiceAccessStrategy","enabled" : true,"ssoEnabled" : true}
}

这里的RegisteredService实现类是 RegexRegisteredService,它通过正则匹配service的url,模式是HTTPSandIMAPS-10000001.json文件中定义的serviceId。

继续分析它是怎么通知应用服务器销毁ticket的。

private boolean performBackChannelLogout(final LogoutRequest request) {try {// 构建登出的协议报文final String logoutRequest = this.logoutMessageBuilder.create(request);final SingleLogoutService logoutService = request.getService();logoutService.setLoggedOutAlready(true);// LogoutHttpMessage封装了请求的url和报文,url就是应用服务器的urlfinal LogoutHttpMessage msg = new LogoutHttpMessage(request.getLogoutUrl(), logoutRequest);// 调用httpClient,以POST的方式发出报文return this.httpClient.sendMessageToEndPoint(msg);} catch (final Exception e) {LOGGER.error(e.getMessage(), e);}return false;
}

报文内容如下:

<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="LR-1-VM1PfgJD6VEDtCc4NnIWaVLqFs0PktY6Ej9" Version="2.0" IssueInstant="2017-07-20T10:45:39Z"><saml:NameID xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">@NOT_USED@</saml:NameID><samlp:SessionIndex>ST-2-HtrBiWrgRD9DFgL25GI9-passport.edu</samlp:SessionIndex>
</samlp:LogoutRequest>

报文是CAS的协议格式,表示现在发的是logout请求,包含了该service的ST。

至此,CAS服务器遍历了所有的sercie,给service发出了退出登录的报文。然后它自己注销删除了TGT。

=========+=======我是分割线,下面是应用服务器端分析=======================

应用服务器通过一个监听器和一个过滤器完成登出功能。

<listener><listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class></listener><filter><filter-name>CAS Single Sign Out Filter</filter-name><filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class><init-param><param-name>casServerUrlPrefix</param-name><param-value>http://passport.edu:18080</param-value></init-param></filter><filter-mapping><filter-name>CAS Single Sign Out Filter</filter-name><url-pattern>/*</url-pattern></filter-mapping>

先看一下 SingleSignOutFilter 的doFilter。

   public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse,final FilterChain filterChain) throws IOException, ServletException {final HttpServletRequest request = (HttpServletRequest) servletRequest;final HttpServletResponse response = (HttpServletResponse) servletResponse;if (!this.handlerInitialized.getAndSet(true)) {HANDLER.init();}// 由HANDLER处理if (HANDLER.process(request, response)) {filterChain.doFilter(servletRequest, servletResponse);}}

HANDLE的实现类是SingleSignOutHandler。看一下它的process方法

public boolean process(final HttpServletRequest request, final HttpServletResponse response) {if (isTokenRequest(request)) {logger.trace("Received a token request");recordSession(request);return true;} else if (isBackChannelLogoutRequest(request)) { //这里这里。。。logger.trace("Received a back channel logout request");destroySession(request);return false;} else if (isFrontChannelLogoutRequest(request)) {logger.trace("Received a front channel logout request");destroySession(request);// redirection url to the CAS serverfinal String redirectionUrl = computeRedirectionToServer(request);if (redirectionUrl != null) {CommonUtils.sendRedirect(response, redirectionUrl);}return false;} else {logger.trace("Ignoring URI for logout: {}", request.getRequestURI());return true;}
}

process方法将会解析报文,获取该报文是什么类型的,前面已经分析过是请求登出报文,我们进入isBackChannelLogoutRequest(request)分支。这里调用了destroySession(request)。

private void destroySession(final HttpServletRequest request) {final String logoutMessage;if (isFrontChannelLogoutRequest(request)) {// 不要理睬,这里前台登出才做的事logoutMessage = uncompressLogoutMessage(CommonUtils.safeGetParameter(request,this.frontLogoutParameterName));} else {// 获取报文的内容logoutMessage = CommonUtils.safeGetParameter(request, this.logoutParameterName, this.safeParameters);}// 获取STfinal String token = XmlUtils.getTextForElement(logoutMessage, "SessionIndex");if (CommonUtils.isNotBlank(token)) {// 缓存中删除ST与sessionId的映射关系,获取sessionfinal HttpSession session = this.sessionMappingStorage.removeSessionByMappingId(token);if (session != null) {final String sessionID = session.getId();try {session.invalidate(); //销毁session} catch (final IllegalStateException e) {logger.debug("Error invalidating session.", e);}this.logoutStrategy.logout(request); //好像用于强制退出}}
}

由于前面是向每个已经在CAS登录的应用服务器发送登出报文的,所以每个应用服务器都会走一次销毁ticket的流程。至此,应用服务器也销毁了ticket,并且session也已经销毁了。

转载于:https://my.oschina.net/thinwonton/blog/1475562

CAS 4.1.x 单点登出(退出登录)的原理解析相关推荐

  1. Springsecurity+cas整合后无法单点登出

    1 问题已在csdn论坛上讨论:https://bbs.csdn.net/topics/392440272 各种思路探讨: 1 将单点登录过滤器放在最前边 2 重新更换注册地址 3 将登出地址设置成无 ...

  2. 搜狗输入法如何登出退出登录和截图识别

    搜狗输入法估计很多人都在用,有还有很多习惯性的词库保存在里面了,所以安装新系统后就会想到要安装一个搜狗,但苦恼的是它的登录和登出都不是很明显能够找到,尤其是登出.这里主要是记录下来,以防以后自己忘记了 ...

  3. CAS学习笔记五:SpringBoot自动/手动配置方式集成CAS单点登出

    本文目标 基于SpringBoot + Maven 分别使用自动配置与手动配置过滤器方式实现CAS客户端登出及单点登出. 本文基于<CAS学习笔记三:SpringBoot自动/手动配置方式集成C ...

  4. CAS单点登出,调整CAS源码,实现前后端分离单点登出、清除redis、shiro登录状态

    前端点击"登出"按钮,跳转到CAS的登出. CAS默认配置了单点登出,在登出后,会向所有客户端系统发送这个用户登出的报文. 各客户端系统有责任接收并处理这个用户登出的报文,然后在注 ...

  5. cas client 更新ticket_cas sso单点登录系列6_cas单点登录防止登出退出后刷新后退ticket失效报500错...

    转(http://blog.csdn.net/ae6623/article/details/9494601) 问题: 我登录了client2,又登录了client3,现在我把client2退出了,在c ...

  6. cas client 更新ticket_SSO单点登录一:cas单点登录防止登出退出后刷新后退ticket失效报500错,也有退出后直接重新登录报票根验证错误...

    问题1: 我登录了client2,又登录了client3,现在我把client2退出了,在client3里面我F5刷新了一下,结果页面报错: 未能够识别出目标 'ST-41-2VcnVMguCDWJX ...

  7. CAS 单点登出失效的问题(源码跟踪)

    一.环境说明 服务端:cas-server-3.5.2 客户端:cas-client-3.2.1+spring mvc 说明:服务端与客户端均是走的Https 客户端配置文件: application ...

  8. CAS单点登出实现案例

    2019独角兽企业重金招聘Python工程师标准>>> 单点登出的实现比较简单,就是简单的几个配置. 如果直接调用cas的logout的url进行登出,则会暴露cas的登出界面 显然 ...

  9. 源代码解读Cas实现单点登出(single sign out)功能实现原理

    关于Cas实现单点登入(single sing on)功能的文章在网上介绍的比较多,想必大家多多少少都已经有所了解,在此就不再做具体介绍.如果不清楚的,那只能等我把single sign on这块整理 ...

最新文章

  1. PHP文件上传,下载,Sql工具类!
  2. 杀毒软件:看企业版与单机版之间区别
  3. 心电图多少为正常范围_研究:心跳超过70次/分,至少减寿3年!正常心率范围是多少?...
  4. Snipaste截图
  5. mysql链路跟踪工具_EasySwoole利用链路追踪组件制作甩锅工具
  6. GIT在Linux上的安装和使用简介
  7. Linux命令行打开不了发行光盘RHEL_6.3 i386 Disc 1
  8. mac系统用什么linux远程工具,推荐几个Mac/Linux下比较好用的工具
  9. html希腊字符,希腊字母
  10. EINT DINT ERTM DRTM理解
  11. 好用的微信群管理软件
  12. hive concat_ws列转行排序问题
  13. JavaWeb查漏补缺
  14. 【绘图】3D点图 及绘图关系matplotlib中plt系列
  15. 力扣启蒙 - 开启算法的世界
  16. 序列化和反序列化的概念及应用
  17. 2020春季《形势与政策》各章节测试答案
  18. 2021年高考成绩查询达州,四川省达州市2021年普通高校招生网上报名入口
  19. Android学习——Shortcut
  20. 添加wellcap和endcap作用

热门文章

  1. Java知多少(79)哈希表及其应用
  2. C++实践笔记(四)----AVL树的简单实现
  3. 【Hadoop】在Linux中的Hadoop部署与yarn HDFS MapReduce 的配置中常见的问题?你解决了吗?
  4. app测试过程和重点关注内容
  5. Servlet 的三种创建方式
  6. linux c curl 乱码,curl获取结果乱码的解决方法之CURLOPT_ENCODING(curl/Post请求)
  7. 一位00后前端2年经验的成长历程
  8. linux中Grep常用的15个例子,Linux中Grep惯用的15个例子
  9. android 灰色向白色渐变,iPhone-iOS的白色到透明渐变层为灰色
  10. 北理工计算机 应用基础在线作业,16秋北理工《计算机应用基础》在线作业