一、CAS简介

CAS 是 Central Authentication Service 的缩写 —— 中央认证服务,一种独立开放指令协议,是 Yale 大学发起的一个企业级开源项目,旨在为 Web 应用系统提供一种可靠的 SSO 解决方案。

img

CAS 支持以下特性:

  • CAS v1, v2 和 v3 协议
  • SAML v1 和 v2 协议
  • OAuth v2 协议
  • OpenID & OpenID Connect 协议
  • WS-Federation Passive Requestor 协议
  • 通过 JAAS, LDAP, RDBMS, X.509, Radius, SPNEGO, JWT, Remote, Trusted, BASIC, Apache Shiro, MongoDb, Pac4J 等组件进行身份验证
  • 将身份验证委派至 WS-FED, Facebook, Twitter, SAML IdP, OpenID, OpenID Connect, CAS 等地方
  • 通过 ABAC, Time/Date, REST, Internet2 的 Grouper 等因子进行身份验证
  • 通过 Hazelcast, Ehcache, JPA, Memcached, Apache Ignite, MongoDb, Redis, DynamoDb, Couchbase 等工具进行 HA 集群部署
  • 由 JSON, LDAP, YAML, JPA, Couchbase, MongoDb, DynamoDb, Redis 等工具支持的应用程序注册
  • 通过 Duo Security, YubiKey, RSA, Google Authenticator 等因子进行多因子身份验证
  • 用于管理日志记录、监控、统计、配置和客户端注册等的管理UI
  • 密码管理和密码策略实施

二、spring boot 2.0 集成 shiro 和 pac4j cas单点登录

参照
spring boot 2.0 集成 shiro 和 pac4j cas单点登录

若依集成cas客户端

网上的资料比较少,都是互相复制、粘贴,本人懒得粘贴,自己去原网址去阅读!

注意:以上参考资料本人尝试了下,单点登出无效!!!

三、解决单点登出失效问题

以下截取部分ShiroConfig配置类代码

1
//cas 资源认证拦截器 SecurityFilter securityFilter = new SecurityFilter(); securityFilter.setConfig(config); securityFilter.setClients(clientName); //cas 认证后回调拦截器 CallbackFilter callbackFilter = new CallbackFilter(); callbackFilter.setDefaultUrl(projectUrl); callbackFilter.setConfig(config); // 注销 拦截器 LogoutFilter logoutFilter = new LogoutFilter(); logoutFilter.setConfig(config); // CAS服务端登出 logoutFilter.setCentralLogout(true); logoutFilter.setLocalLogout(true); logoutFilter.setDefaultUrl(projectUrl + "/callback?client_name=" + clientName);

1.单点登出流程

CAS单点登录 登出流程图
单点登录

单点登出

2.排查单点登出客户端cas.callbackUrl是否出现localhost

-———————————————–划重点(开始)-———————————————–

  • client-host-url支持域名、主机名、IP地址。CAS服务端对client-host-url有严格的校验方法(包括正则校验、顶级域名校验、IPv4、IPv6校验)。只要搞清楚原理,你会发现官方的考虑还是很周全的,一般情况下,根本不需要自己重写具体实现类。
1
// url校验入口方法 org.apereo.cas.logout.DefaultSingleLogoutServiceLogoutUrlBuilder#determineLogoutUrl
  • 域名校验(含顶级域名校验)
1
// cas.callbackUrl的默认校验方法, org.apereo.cas.web.SimpleUrlValidator#isValid // SimpleUrlValidator对应的工厂类bean(cas-server-core-web包下的CasCoreWebConfiguration.java配置类注入该bean) org.apereo.cas.web.SimpleUrlValidatorFactoryBean // 域名校验 org.apache.commons.validator.routines.DomainValidator#isValid // 顶级域名校验(以点号分割取最后一部分,常见的顶级域名有.com、.cn、.gov、.org、.edu) // 本人测试时使用cas.example.client,可想而知肯定验证不通过!!!建议使用client1.example.org来测试。 org.apache.commons.validator.routines.DomainValidator#isValidTld
  • 主机名校验
    对于localhost和localhost.localdomain,要想验证通过,一定要在CAS服务端配置以下属性。否则配置cas.callbackUrl配置为localhost会导致单证登出不生效!
1
# 官网属性配置:https://apereo.github.io/cas/5.3.x/installation/Configuration-Properties.html cas: httpClient: hostNameVerifier: NONE # 启用域名本地验证,默认false allowLocalLogoutUrls: true # 自定义校验(为空时忽略该属性,默认即为空) authorityValidationRegEx: authorityValidationRegExCaseSensitive: false
  • IP校验
    以IPv4为例,可配置127.0.0.1,没什么可讲的!

客户端示例:

1
# 默认不支持域名本地验证,需CAS服务端启用 cas: server-login-url: https://cas.example.org:8443/cas/login callbackUrl: http://client1.example.org:80/callback client-name: client1

总结:域名配置要按规矩来,不要像我一样配置成cas.example.client,查错查半天!!!

-———————————————–划重点(结束)-———————————————–
如果想禁用url校验,可以通过自己实现接口SingleLogoutServiceLogoutUrlBuilder,代码如下:

1
package com.example.cas.config; import org.apereo.cas.configuration.CasConfigurationProperties; import org.apereo.cas.logout.SingleLogoutServiceLogoutUrlBuilder; import org.apereo.cas.web.UrlValidator; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * @version 1.0 * @date :Created in 2020/11/11 14:50 * 功能描述:单点登出逻辑 {@link org.apereo.cas.logout.config.CasCoreLogoutConfiguration} */ @Configuration("CustomLogoutConfiguration") @EnableConfigurationProperties(CasConfigurationProperties.class) public class CustomLogoutConfiguration { @Autowired private UrlValidator urlValidator; @Bean public SingleLogoutServiceLogoutUrlBuilder singleLogoutServiceLogoutUrlBuilder() { return new CustomSingleLogoutServiceLogoutUrlBuilder(this.urlValidator); } } package com.example.cas.config; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.apereo.cas.authentication.principal.WebApplicationService; import org.apereo.cas.logout.SingleLogoutServiceLogoutUrlBuilder; import org.apereo.cas.services.RegisteredService; import org.apereo.cas.util.CollectionUtils; import org.apereo.cas.web.UrlValidator; import java.net.URL; import java.util.Collection; /** * @version 1.0 * @date :Created in 2020/11/11 14:59 * 功能描述: */ @Slf4j @RequiredArgsConstructor public class CustomSingleLogoutServiceLogoutUrlBuilder implements SingleLogoutServiceLogoutUrlBuilder { private final UrlValidator urlValidator; @Override @SneakyThrows public Collection<URL> determineLogoutUrl(final RegisteredService registeredService, final WebApplicationService singleLogoutService) { final URL serviceLogoutUrl = registeredService.getLogoutUrl(); if (serviceLogoutUrl != null) { log.debug("Logout request will be sent to [{}] for service [{}]", serviceLogoutUrl, singleLogoutService); return CollectionUtils.wrap(serviceLogoutUrl); } final String originalUrl = singleLogoutService.getOriginalUrl(); // TODO 通过禁用url校验,使得CAS服务端发送退出请求给CAS客户端 // if (this.urlValidator.isValid(originalUrl)) { log.debug("Logout request will be sent to [{}] for service [{}]", originalUrl, singleLogoutService); final URL url = new URL(originalUrl); return CollectionUtils.wrap(url); // } // log.debug("Logout request will not be sent; The URL [{}] for service [{}] is not valid", originalUrl, singleLogoutService); // return new ArrayList<>(); } }

3.排查Pac4jConfig文件SessionStore类型

以下截取Pac4jConfig配置文件部分内容

1
/** * 自定义存储 * 注意:不可使用{@link io.buji.pac4j.context.ShiroSessionStore},否则无法建立票据与session之间联系,导致单点退出失效! * @return */ @Bean public SessionStore sessionStore(){ return new J2ESessionStore(); }

:为什么不能用ShiroSessionStore实现类?
:因为io.buji.pac4j.context.ShiroSessionStore#getTrackableSession方法返回值始终为null,CAS客户端源码打断点查看你就明白了!

1
io.buji.pac4j.context.ShiroSessionStore#getTrackableSession org.pac4j.cas.logout.DefaultCasLogoutHandler#recordSession org.pac4j.cas.logout.DefaultCasLogoutHandler#destroySessionBack

  • 1.cas.callbackUrl配置成localhost时错误日志
1
2020-11-11 09:11:22,362 INFO [org.apereo.cas.logout.DefaultLogoutManager] - <Performing logout operations for [TGT-1-********************************************************8c-MzwaUSIQLAPTOP-O5TBUP89]> 2020-11-11 09:11:22,368 DEBUG [org.apereo.cas.logout.DefaultLogoutManager] - <Handling single logout callback for [AbstractWebApplicationService(id=http://localhost:80/callback?client_name=client1, originalUrl=http://localhost:80/callback?client_name=client1, artifactId=null, principal=9, source=service, loggedOutAlready=false, format=XML, attributes={})]> 2020-11-11 09:11:22,368 DEBUG [org.apereo.cas.logout.DefaultSingleLogoutServiceMessageHandler] - <Processing logout request for service [AbstractWebApplicationService(id=http://localhost:80/callback?client_name=client1, originalUrl=http://localhost:80/callback?client_name=client1, artifactId=null, principal=9, source=service, loggedOutAlready=false, format=XML, attributes={})]...> 2020-11-11 09:11:22,368 DEBUG [org.apereo.cas.logout.DefaultSingleLogoutServiceMessageHandler] - <Service [AbstractWebApplicationService(id=http://localhost:80/callback?client_name=client1, originalUrl=http://localhost:80/callback?client_name=client1, artifactId=null, principal=9, source=service, loggedOutAlready=false, format=XML, attributes={})] supports single logout and is found in the registry as [AbstractRegisteredService(serviceId=^(https|imaps|http)://(localhost|cas.example.org).*, name=自定义主题, theme=simple, informationUrl=null, privacyUrl=null, responseType=null, id=1000, description=this is a localhost service, expirationPolicy=DefaultRegisteredServiceExpirationPolicy(deleteWhenExpired=false, notifyWhenDeleted=false, expirationDate=null), proxyPolicy=org.apereo.cas.services.RefuseRegisteredServiceProxyPolicy@1, evaluationOrder=1000, usernameAttributeProvider=org.apereo.cas.services.DefaultRegisteredServiceUsernameProvider@87297e2, logoutType=BACK_CHANNEL, requiredHandlers=[], attributeReleasePolicy=ReturnAllAttributeReleasePolicy(super=AbstractRegisteredServiceAttributeReleasePolicy(attributeFilter=null, principalAttributesRepository=DefaultPrincipalAttributesRepository(), consentPolicy=DefaultRegisteredServiceConsentPolicy(enabled=true, excludedAttributes=null, includeOnlyAttributes=null), authorizedToReleaseCredentialPassword=false, authorizedToReleaseProxyGrantingTicket=false, excludeDefaultAttributes=false, authorizedToReleaseAuthenticationAttributes=true, principalIdAttribute=null)), multifactorPolicy=DefaultRegisteredServiceMultifactorPolicy(multifactorAuthenticationProviders=[], failureMode=NOT_SET, principalAttributeNameTrigger=null, principalAttributeValueToMatch=null, bypassEnabled=false), logo=null, logoutUrl=null, accessStrategy=DefaultRegisteredServiceAccessStrategy(order=0, enabled=true, ssoEnabled=true, unauthorizedRedirectUrl=null, delegatedAuthenticationPolicy=DefaultRegisteredServiceDelegatedAuthenticationPolicy(allowedProviders=[]), requireAllAttributes=true, requiredAttributes={}, rejectedAttributes={}, caseInsensitive=false), publicKey=null, properties={}, contacts=[])]. Proceeding...> 2020-11-11 09:11:22,457 DEBUG [org.apereo.cas.logout.DefaultSingleLogoutServiceLogoutUrlBuilder] - <Logout request will not be sent; The URL [http://localhost:80/callback?client_name=client1] for service [AbstractWebApplicationService(id=http://localhost:80/callback?client_name=client1, originalUrl=http://localhost:80/callback?client_name=client1, artifactId=null, principal=9, source=service, loggedOutAlready=false, format=XML, attributes={})] is not valid> 2020-11-11 09:11:22,457 DEBUG [org.apereo.cas.logout.DefaultSingleLogoutServiceMessageHandler] - <Prepared logout url [[]] for service [AbstractWebApplicationService(id=http://localhost:80/callback?client_name=client1, originalUrl=http://localhost:80/callback?client_name=client1, artifactId=null, principal=9, source=service, loggedOutAlready=false, format=XML, attributes={})]> 2020-11-11 09:11:22,457 DEBUG [org.apereo.cas.logout.DefaultSingleLogoutServiceMessageHandler] - <Service [AbstractWebApplicationService(id=http://localhost:80/callback?client_name=client1, originalUrl=http://localhost:80/callback?client_name=client1, artifactId=null, principal=9, source=service, loggedOutAlready=false, format=XML, attributes={})] does not support logout operations given no logout url could be determined.> 2020-11-11 09:11:22,457 DEBUG [org.apereo.cas.logout.DefaultLogoutManager] - <Invoking logout handler [CasCoreLogoutConfiguration$$Lambda$217/1552758845] to process ticket [TGT-1-********************************************************8c-MzwaUSIQLAPTOP-O5TBUP89]> 2020-11-11 09:11:22,458 INFO [org.apereo.cas.logout.DefaultLogoutManager] - <[0] logout requests were processed>
  • 使用io.buji.pac4j.context.ShiroSessionStore类作为session存储实现类时
1
9:26:46.870 [http-nio-80-exec-9] ERROR o.p.c.l.DefaultCasLogoutHandler - [destroySessionBack,99] - No trackable session found for back channel logout. Either the session store does not support to track session or it has expired from the store and the store settings must be updated (expired data)