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

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

时间:2024-02-22 12:24:21

相关推荐

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

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

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

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

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

org.jasig.cas.client.session.SingleSignOutHttpSessionListener

CAS Single Sign Out Filter

org.jasig.cas.client.session.SingleSignOutFilter

casServerUrlPrefix

http://passport.edu:18080

CAS Single Sign Out Filter

/*

CAS Authentication Filter

org.jasig.cas.client.authentication.AuthenticationFilter

casServerLoginUrl

http://passport.edu:18080/login

serverName

http://jd.edu:9443

CAS Authentication Filter

/groupon/*

CAS Validation Filter

org.jasig.cas.client.validation.Cas30ProxyReceivingTicketValidationFilter

casServerUrlPrefix

http://passport.edu:18080

serverName

http://jd.edu:9443

redirectAfterValidation

true

CAS Validation Filter

/*

CAS HttpServletRequest Wrapper Filter

org.jasig.cas.client.util.HttpServletRequestWrapperFilter

CAS HttpServletRequest Wrapper Filter

/*

(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建立映射关系,该映射关系将会在单点登出的时候使用。

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 destroyTicketGrantingTicket(@NotNull final String ticketGrantingTicketId) {

try {

// 根据tgt ID从ticketRegistry注册中心中获取TGT

final TicketGrantingTicket ticket = getTicket(ticketGrantingTicketId, TicketGrantingTicket.class);

// 备注(1):由LogoutManager 完成注销

final List logoutRequests = logoutManager.performLogout(ticket);

// 备注(2):注册中心删除该tgt

this.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 performLogout(final TicketGrantingTicket ticket) {

final Map services = ticket.getServices(); // 获取注册在tgt下的service

final List logoutRequests = new ArrayList<>();

if (!this.singleLogoutCallbacksDisabled) {

// 遍历所有的service

for (final Map.Entry entry : services.entrySet()) {

// it's a SingleLogoutService, else ignore

final 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:

// 通知应用服务器注销ticket

if (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就是应用服务器的url

final 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;

}

报文内容如下:

@NOT_USED@

ST-2-HtrBiWrgRD9DFgL25GI9-passport.edu

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

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

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

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

org.jasig.cas.client.session.SingleSignOutHttpSessionListener

CAS Single Sign Out Filter

org.jasig.cas.client.session.SingleSignOutFilter

casServerUrlPrefix

http://passport.edu:18080

CAS Single Sign Out Filter

/*

先看一下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 server

final 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);

}

// 获取ST

final String token = XmlUtils.getTextForElement(logoutMessage, "SessionIndex");

if (CommonUtils.isNotBlank(token)) {

// 缓存中删除ST与sessionId的映射关系,获取session

final 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也已经销毁了。

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