1200字范文,内容丰富有趣,写作的好帮手!
1200字范文 > CAS 4.1.x 单点登出(退出登录)的原理解析

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

时间:2020-05-23 22:29:08

相关推荐

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

独角兽企业重金招聘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方法。

@Overridepublic 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="-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也已经销毁了。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。