SSO CAS

主要介绍CAS SSO的认证流程。有关这方面的内容再网上也有很多资料,写这篇总结目的一来是自己在理解这块内容的时候觉得专业术语有点多,理来理去有点绕,想通过总结来加强自己对这一块内容的理解;其次,网上的资料虽然很多,但是很多都不是很全面,而这篇总结刚好可以在前人的基础上进行过总结,结合网上的一些资料,再加上自己的 一些理解,应该可以更快的掌握这一块内容。这篇总结主要是一些理论层面的内容,请看其它两篇文章。

相关链接:
http://blog.csdn.net/gzseehope/article/details/72914188

http://blog.csdn.net/tyt_venice/article/details/56675186

http://blog.csdn.net/javaloveiphone/article/details/52439613

http://www.imooc.com/article/3558

  • CAS ( Central Authentication Service ) 是 Yale 大学发起的一个企业级的、开源的项目,旨在为 Web 应用系统提供一种可靠的单点登录解决方法(属于 Web SSO)。CAS 开始于 2001 年, 并在 2004 年 12 月正式成为 JA-SIG 的一个项目。
  • SSO 单点登录(Single Sign-On)是目前比较流行的服务于企业业务整合的解决方案之一, SSO 使得在多个应用系统中,用户只需要 登录一次 就可以访问所有相互信任的应用系统。
  • CAS 包括两部分: CAS Server 和 CAS Client 。CAS Server 负责完成对用户的认证工作 , 需要独立部署 , CAS Server 会处理用户名 / 密码等凭证(Credentials) 。CAS Client与受保护的客户端应用部署在一起,以 Filter 方式保护受保护的资源。负责处理对客户端受保护资源的访问请求,需要对请求方进行身份认证时,重定向到 CAS Server 进行认证。(原则上,客户端应用不再接受任何的用户名密码等 Credentials)。
  • CAS基础模式

    先看看网上的一个CAS基本协议过程:

  • 用户认证:用户身份认证。
  • 发放票据: SSO 服务器会产生一个随机的 Service Ticket 。
  • 验证票据: SSO 服务器验证票据 ST 的合法性,验证通过后,允许客户端访问服务。
  • 传输用户信息: SSO 服务器验证票据通过后,传输用户认证结果信息给客户端。
  • CAS Client 与受保护的客户端应用部署在一起,以 Filter 方式保护 Web 应用的受保护资源,过滤从客户端过来的每一个 Web 请求,同时, CAS Client 会分析 HTTP 请求中是否包含请求 Service Ticket( ST 上图中的 Ticket) ,如果没有,则说明该用户是没有经过认证的;于是 CAS Client 会重定向用户请求到 CAS Server ( Step 2 ),并传递 Service (要访问的目的资源地址)。 Step 3 是用户认证过程,如果用户提供了正确的 Credentials , CAS Server 随机产生一个相当长度、唯一、不可伪造的 Service Ticket ,并缓存以待将来验证,并且重定向用户到 Service 所在地址(附带刚才产生的 Service Ticket ) , 并为客户端浏览器设置一个 Ticket Granted Cookie ( TGC ) ; CAS Client 在拿到 Service 和新产生的 Ticket 过后,在 Step 5 和 Step6 中与 CAS Server 进行身份核实,以确保 Service Ticket 的合法性。在该协议中,所有与 CAS Server 的交互均采用 SSL 协议,以确保 ST 和 TGC 的安全性。协议工作过程中会有 2 次重定向 的过程。但是 CAS Client 与 CAS Server 之间进行 Ticket 验证的过程对于用户是透明的(使用 HttpsURLConnection )。

    应用系统将登录请求转给认证中心,这个很好解决,我们一个HTTP重定向即可实现。现在的问题是,用户在认证中心登录后,认证中心如何将消息转回给该系统?这是在单web系统中不存在的问题。我们知道HTTP协议传递消息只能通过请求参数方式或cookie方式,cookie跨域问题不能解决,我们只能通过URL请求参数。我们可以将认证通过消息做成一个令牌(token)再利用HTTP重定向传递给应用系统。但现在的关键是:该系统如何判断这个令牌的真伪?如果判断这个令牌确实是由认证中心发出的,且是有效的?我们还需要应用系统和认证中心之间再来个直接通信,来验证这个令牌确实是认证中心发出的,且是有效的。由于应用系统和认证中心是属于服务端之间的通信,不经过用户浏览器,相对是安全的。

    假如现在应用集群中又两个系统A、B。当客户首次登录A系统的时候,流程如下:

  • 用户浏览器访问系统A需登录受限资源。
  • 系统A发现该请求需要登录,将请求重定向到认证中心,进行登录。
  • 认证中心呈现登录页面,用户登录,登录成功后,认证中心重定向请求到系统A,并附上认证通过令牌。
  • 系统A与认证中心通信,验证令牌有效,证明用户已登录。
  • 系统A将受限资源返给用户。
  • 登录状态判断

    用户到认证中心登录后,用户和认证中心之间建立起了会话,我们把这个会话称为全局会话。当用户后续访问系统应用时,我们不可能每次应用请求都到认证中心去判定是否登录,这样效率非常低下,这也是单Web应用不需要考虑的。我们可以在系统应用和用户浏览器之间建立起局部会话,局部会话保持了客户端与该系统应用的登录状态,局部会话依附于全局会话存在,全局会话消失,局部会话必须消失。用户访问应用时,首先判断局部会话是否存在,如存在,即认为是登录状态,无需再到认证中心去判断。如不存在,就重定向到认证中心判断全局会话是否存在,如存在,按1提到的方式通知该应用,该应用与客户端就建立起它们之间局部会话,下次请求该应用,就不去认证中心验证了。

    用户在一个系统登出了,访问其它子系统,也应该是登出状态。要想做到这一点,应用除结束本地局部会话外,还应该通知认证中心该用户登出。认证中心接到登出通知,即可结束全局会话,同时需要通知所有已建立局部会话的子系统,将它们的局部会话销毁。这样,用户访问其它应用时,都显示已登出状态。
    整个登出流程如下:
    1.客户端向应用A发送登出Logout请求。
    2.应用A取消本地会话,同时通知认证中心,用户已登出。
    3.应用A返回客户端登出请求。
    4.认证中心通知所有用户登录访问的应用,用户已登出。

    基础模式总结

    虽然很多东西都是从网上查过来的,但是对我们理解这块的内容却非常有帮助。当然,通过上面的内容,基本上额可以了解CAS SSO的基本原理,但还是有一些不明白的地方,因此还是有必要用自己的话总结一遍。
    几个注意点:

  • 用户认证操作是 完全交给认证中心的。比如用户在浏览器上输入的用户名、密码,这些信息按理来说是完全可以不和你的应用系统打交道的,因为认证操作完全是由认证中心(也就是 CAS Server)来处理的。
  • 全局会话是是由用户和认证中心建立的,这个全局会话就是TGT(Ticket Granting Ticket),或者说:登录成功后,认证中心会产生一个票据叫TGT(Ticket Granting Ticket),TGT即代表了用户与认证中心直接的全局会话,TGT存在,表明该用户处于登录状态。TGT位于CAS服务器端,TGT并没有放在Session中,也就是说,CAS全局会话的实现并没有直接使用Session机制,而是利用了Cookie自己实现的,这个Cookie叫做TGC(Ticket Granting Cookie),它存放了TGT的id,保存在用户浏览器上,CAS Server实现了TGT。
  • 局部会话是由浏览器和应用系统之间建立的。并且局部会话必须依赖于全局会话。创建局部会话,相当于是在浏览器和因够用系统之间创建了一个session。一般是在用户首次登录的时候创建的(是这样吗?)。
  • 假如现在有A、B两个系统属于同一应用群,当用户向系统A发送一个请求的时候,大概是这样一个过程:
    1、 用户通过浏览器访问系统A,系统A(也可以称为CAS客户端)去Cookie中拿JSESSION,即在Cookie中维护的当前回话session的id,如果拿到了,说明用户已经登录,这时候直接返回请求的资源,这里没有啥好讨论的。如果未拿到,说明用户未登录,主要讨论这一块内容。

    2、 如果发现用户未登录。系统A就重定向到认证中心(CAS 服务端),并将当前客户端的地址作为参数传递到认证中心(以便于认证中心生成ST之后返回当前地址),假如系统A的地址为 http://a:8080/ ,CAS认证中心的服务地址为 http://cas.server:8080/ ,那么重点向前后地址变化为:由 http://a:8080/ http://cas.server:8080/?service=http://a:8080/ ,由此可知,重点向到认证中心,认证中心拿到了当前访问客户端的地址。

    3、 重定向到认证中心后,认证中心首先会去浏览器拿全局会话id(也就是TGT,TGC中存放了TGT的id),如果不存在就进入认证中心的登录界面,用户输入用户名、密码进行登录,登录成功后,CAS 服务端创建全局会话TGT,并且将TGT最为缓存在cookie中(TGC),同时会产生一个随机的 Service Ticket (ST),并且重定向到A系统,并且携带ST,重定向之后的地址变为: http://a:8080/?ticket=ST-XXXX-XXX

    4、 系统A通过地址栏获取ticket的参数值ST票据,然后从后台将ST发送给CAS server认证中心验证,验证ST有效后,CAS server返回当前用户登录的相关信息,系统A接收到返回的用户信息,并为该用户创建session会话(局部会话),会话id由cookie维护,来证明其已登录。

    5、 系统A将资源返回给 浏览器。
    至此,用户向系统A发送一个请求的流程已经完成。这个时候,如果用户向系统A发送另外一个请求,因为系统A和浏览器之间已经创建了一个局部会话,因此这时候会直接返回资源。

    假如还是同一个用户,向系统B发送一个请求,流程是什么样的?

    1、 在系统A登录成功后,用户和认证中心之间建立起了全局会话,这个全局会话就是TGT(Ticket Granting Ticket),TGT位于CAS服务器端,TGT并没有放在Session中,也就是说,CAS全局会话的实现并没有直接使用Session机制,而是利用了Cookie自己实现的,这个Cookie叫做TGC(Ticket Granting Cookie),它存放了TGT的id,保存在用户浏览器上。用户发送登录系统B的请求,首先会去Cookie中拿JSESSION,因为系统B并未登录过,session会话还未创建,JSESSION的值是拿不到的,然后将请求重定向到CAS认证中心,CAS认证中心先去用户浏览器中拿TGC的值,也就是全局会话id,如果存在则代表用户在认证中心已经登录,附带上认证令牌重定向到系统B。如果全局会话不存在,那这是就好执行一次建立全局会话的过程。

    2、 系统B拿到ST后,也会在系统B和浏览器之间建立一个局部会话。

    3、 将系统B的资源返回给浏览器。

    认证中心清除当前用户的全局会话TGT,同时清掉cookie中TGT的id:TGC;
    然后是各个客户端系统,比如系统A、系统B,清除局部会话session,同时清掉cookie中session会话id:jsession。

    CAS代理模式

    用网上找到的一些概念:代理模式形式为用户访问App1,App1又依赖于App2来获取一些信息,如:User -->App1 -->App2 。这种情况下,假设App2也是需要对User进行身份验证才能访问,那么,为了不影响用户体验(过多的重定向导致User的IE窗口不停地闪动),CAS引入了一种Proxy认证机制,即CAS Client可以代理用户去访问其它Web应用。代理的前提是需要CAS Client拥有用户的身份信息(类似凭据)。之前我们提到的TGC是用户持有对自己身份信息的一种凭据,这里的PGT就是CAS Client端持有的对用户身份信息的一种凭据。凭借TGC,User可以免去输入密码以获取访问其它服务的Service Ticket,所以,这里凭借PGT,Web应用可以代理用户去实现后端的认证,而无需前端用户的参与。下面为代理应用(helloService)获取PGT的过程:(注:PGTURL用于表示一个Proxy服务,是一个回调链接;PGT相当于代理证;PGTIOU为取代理证的钥匙,用来与PGT做关联关系;)

    CAS Client 在基础协议之上,在验证ST时提供了一个额外的PGT URL(而且是 SSL 的入口)给CAS Server,使得CAS Server可以通过PGT URL提供一个PGT给CAS Client。
    CAS Client拿到了PGT(PGTIOU-85…..ti2td),就可以通过PGT向后端Web应用进行认证。
    下面是代理认证和提供服务的过程:

    与基础模式的异同

    首先要明白一点:基础模式是用户对自己信息的一种凭据;而代理模式是CAS Client对用户信息的一种凭据。代理模式认证与普通模式的认证其实差别不大,第一步和第二步与基础模式的第一步和第二步几乎一样,唯一不同的是,代理模式用的是PGT而不是TGC,是Proxy Ticket(PT)而不是Service Ticket(ST)。

    CAS Server

    主要介绍CAS Server的简单搭建过程。感觉在实际开发过程中,应该更多的是处理一个CAS Client的集成问题吧?但也还是简单的了解一下CAS Server的搭建过程吧。

    相关链接:
    http://www.cnblogs.com/rwxwsblog/p/4954795.html

    http://www.jianshu.com/p/67d10532c5f8

  • 系统:win10
  • JDK: jdk1.8.0_101
  • CAS Server:cas-server-4.0.0-release.zip
    前提条件,JDK和tomcat已经配置成功。
  • keytool -genkey -alias castest -keyalg RSA -keystore D:/keys/castest
    

    生成一个别名为castest的证书。
    此处需要特别注意口令(后续导入导出证书、CAS服务器端均要用到此口类)和“名字与姓氏”(为CAS跳转域名,否则会报错)

    keytool -export -file F:/keys/castest.crt -alias castest -keystore D:/keys/castest
    

    将证书导入到客户端JRE

    将证书导入到客户端JRE中(注意、是导入JRE中),如果security中已经存在cacerts,需要先将其删除。
    keytool -import -keystore "C:\Program Files\Java\jdk1.8.0_101\jre\lib\security\cacerts" -file D:/keys/castest.crt -alias castest
    这里由一个需要注意的地方,因为我的JDK是安装在C盘(系统盘),而你往系统盘里面写入文件是需要权限的,所以,这条命令可能需要用管理员权限运行。
    如果看过如下错误,请使用管理员权限运行:

    用户密码和数据库交互

    假如需要和数据库交互,应该如何配置呢?因为我本地使用的是mysql,所以需要将以下几个jar包放到WEB-INF/lib目录下:
    mysql-connector-java-5.1.34.jar
    c3p0-0.9.1.1.jar
    cas-server-support-jdbc-4.0.0.jar
    其中的cas-server-support-jdbc-4.0.0.jar我是直接从module目录下拷贝过来的,我们这里 是直接拷贝到该目录下,也有其他的方法,因为cas server本来就一个maven项目,也可以自己修改pom.xml文件,然后重新打包就好了。

    CAS Client

    主要介绍CAS Client的简单搭建过程,多个客户端实现单点登录。相关链接:

    http://www.cnblogs.com/rwxwsblog/p/4954795.html

    http://www.jianshu.com/p/67d10532c5f8

    http://blog.csdn.net/zhmz1326/article/details/52279740

  • 系统:win10
  • JDK: jdk1.8.0_101
  • Tomcat:tomcat8
  • CAS Server:cas-server-4.0.0-release.zip
  • 准备tomcat

    为了演示多个客户端的单点登录功能,所以客户端至少需要两个。所以本地只使用2个tomcat客户端来测试。首先,客户端应用是要和CAS服务端进行交互的,所以这里需要jar文件,放在客户端应用的lib目录下。分别是:cas-client-core-3.2.1.jar、commons-logging.jar。这里直接使用tomcat默认自带的 webapps\examples 作为演示的简单web项目。
    目录结构如下:

    Tomcat解压之后,现在还无法同CAS Server交互,还缺少两个jar包,分别是:

    cas-client-core-3.2.0.jar、

    commons-logging-1.1.jar

    可以从:http://developer.jasig.org/cas-clients/下载。(注意,这里版本不对可能会出异常,比如我一开始用的是3.33版本,在配置filter之后就发现不能正常访问)。

    需要放到客户端应用的lib目录下面。我这里的目录分别是:
    D:\GreenSoft\Apach\Tomcat\Tomcat-8.0.30-x64-client1\webapps\examples\WEB-INF\lib
    D:\GreenSoft\Apach\Tomcat\Tomcat-8.0.30-x64-client2\webapps\examples\WEB-INF\lib

    客户端添修改端口

    因为我们是在同一台机器上运行,然后tomcat默认的端口是8080,如果不修改tomcat端口,在启动CAS Server之后,其他的两个tomcat 客户端就无法启动了,因为端口被占用了,所以这里需要修改两个tommcat 客户端的端口 。
    修改client1端口:
    编辑D:\GreenSoft\Apach\Tomcat\Tomcat-8.0.30-x64-client1\conf\server.xml文件,找到如下内容:

    配置host

    网上说:因为CAS单点登录系统是基于JAVA安全证书的 https 访问, 要使用CAS单点登录必须要配置域名, cas是不能通过ip访问的。首先cas只能通过域名来访问,不能通过ip访问,同时cas Server是生成证书yiduan,所以要求比较严格,所以如果不这么做的话,及时最终按照教程配置完成,cas也可以正常访问,访问一个客户端应用虽然能进入cas验证首页,但是,当输入信息正确后,cas在回调转入你想访问的客户端应用的时候,会出现No subject alternative names present错误异常信息,这个错误也就是在上面输入的第一个问题答案不是域名导致、或者与hosts文件配置的不一致导致。
    不是理解的很好,所以这里还是配置一下host吧。下面3个ip都是127.0.0.1,因为环境都是在同一台机器,所以ip都是一致的,我们再把不同的服务端和客户端应用,使用不同域名加以区分。一个域名对应一个应用,模拟多端

    sso.castest..com =>> 对应部署cas server的tomcat,这个虚拟域名还用于服务端证书生成
    client1.castest..com =>> 对应部署client1客户端应用的tomcat
    client2.castest..com =>> 对应部署client2客户端应用的tomcat

    启动tomcat测试

    这里的测试仅仅是测试两个tomcat客户端是否能够成功启动,并不涉及到和CAS Server相关的内容。

    分别进入:D:\GreenSoft\Apach\Tomcat\Tomcat-8.0.30-x64-client1\bin 和
    D:\GreenSoft\Apach\Tomcat\Tomcat-8.0.30-x64-client2\bin ,并执行 startup.bat

    在浏览器分地址栏分别输入:
    http://client1.castest.com:18080/examples/servlets/

    http://client2.castest.com:28080/examples/servlets/

    可以看到如下界面,说明tomcat客户端启动成功:

    客户端和CAS Server连接

    配置 client1:
    编辑 D:\GreenSoft\Apach\Tomcat\Tomcat-8.0.30-x64-client1\webapps\examples\WEB-INF\web.xml 配置文件,添加如下内容

    <!-- ======================== 单点登录开始 ======================== -->
        <!-- 用于单点退出,该过滤器用于实现单点登出功能,可选配置-->
        <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>
        </filter>
        <filter-mapping>
            <filter-name>CAS Single Sign Out Filter</filter-name>
            <url-pattern>/*</url-pattern>
        </filter-mapping>
        <!-- 该过滤器负责用户的认证工作,必须启用它 -->
        <filter>
            <filter-name>CASFilter</filter-name>
            <filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>
            <init-param>
                <param-name>casServerLoginUrl</param-name>
                <param-value>https://sso.castest.com:8443/cas/login</param-value>
                <!--这里的server是服务端的IP-->
            </init-param>
            <init-param>
                <param-name>serverName</param-name>
                <param-value>http://client1.castest.com:18080</param-value>
            </init-param>
        </filter>
        <filter-mapping>
            <filter-name>CASFilter</filter-name>
            <url-pattern>/*</url-pattern>
        </filter-mapping>
        <!-- 该过滤器负责对Ticket的校验工作,必须启用它 -->
        <filter>
            <filter-name>CAS Validation Filter</filter-name>
            <filter-class>org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter</filter-class>
            <init-param>
                <param-name>casServerUrlPrefix</param-name>
                 <param-value>https://sso.castest.com:8443/cas/</param-value><!-- 此处必须为登录url /cas/,带有任何其它路径都会报错,如“https://sso.castest.com:8443/cas/login”,这样也会报错。 -->
            </init-param>
            <init-param>
                <param-name>serverName</param-name>
                <param-value>http://client1.castest.com:18080</param-value>
            </init-param>
        </filter>
        <filter-mapping>
            <filter-name>CAS Validation Filter</filter-name>
            <url-pattern>/*</url-pattern>
        </filter-mapping>
            该过滤器负责实现HttpServletRequest请求的包裹,
            比如允许开发者通过HttpServletRequest的getRemoteUser()方法获得SSO登录用户的登录名,可选配置。
        <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>
             该过滤器使得开发者可以通过org.jasig.cas.client.util.AssertionHolder来获取用户的登录名。
            比如AssertionHolder.getAssertion().getPrincipal().getName()。
        <filter>
            <filter-name>CAS Assertion Thread Local Filter</filter-name>
            <filter-class>org.jasig.cas.client.util.AssertionThreadLocalFilter</filter-class>
        </filter>
        <filter-mapping>
            <filter-name>CAS Assertion Thread Local Filter</filter-name>
            <url-pattern>/*</url-pattern>
        </filter-mapping>
        <!-- ======================== 单点登录结束 ======================== -->
    

    配置 client2:
    编辑 D:\GreenSoft\Apach\Tomcat\Tomcat-8.0.30-x64-client2\webapps\examples\WEB-INF\web.xml 配置文件,添加如下内容

    <!-- ======================== 单点登录开始 ======================== -->
        <!-- 用于单点退出,该过滤器用于实现单点登出功能,可选配置-->
        <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>
        </filter>
        <filter-mapping>
            <filter-name>CAS Single Sign Out Filter</filter-name>
            <url-pattern>/*</url-pattern>
        </filter-mapping>
        <!-- 该过滤器负责用户的认证工作,必须启用它 -->
        <filter>
            <filter-name>CASFilter</filter-name>
            <filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>
            <init-param>
                <param-name>casServerLoginUrl</param-name>
                <param-value>https://sso.castest.com:8443/cas/login</param-value>
                <!--这里的server是服务端的IP-->
            </init-param>
            <init-param>
                <param-name>serverName</param-name>
                <param-value>http://client2.castest.com:28080</param-value>
            </init-param>
        </filter>
        <filter-mapping>
            <filter-name>CASFilter</filter-name>
            <url-pattern>/*</url-pattern>
        </filter-mapping>
        <!-- 该过滤器负责对Ticket的校验工作,必须启用它 -->
        <filter>
            <filter-name>CAS Validation Filter</filter-name>
            <filter-class>org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter</filter-class>
            <init-param>
                <param-name>casServerUrlPrefix</param-name>
                 <param-value>https://sso.castest.com:8443/cas/</param-value><!-- 此处必须为登录url /cas/,带有任何其它路径都会报错,如“https://sso.castest.com:8443/cas/login”,这样也会报错。 -->
            </init-param>
            <init-param>
                <param-name>serverName</param-name>
                <param-value>http://client2.castest.com:28080</param-value>
            </init-param>
        </filter>
        <filter-mapping>
            <filter-name>CAS Validation Filter</filter-name>
            <url-pattern>/*</url-pattern>
        </filter-mapping>
            该过滤器负责实现HttpServletRequest请求的包裹,
            比如允许开发者通过HttpServletRequest的getRemoteUser()方法获得SSO登录用户的登录名,可选配置。
        <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>
             该过滤器使得开发者可以通过org.jasig.cas.client.util.AssertionHolder来获取用户的登录名。
            比如AssertionHolder.getAssertion().getPrincipal().getName()。
        <filter>
            <filter-name>CAS Assertion Thread Local Filter</filter-name>
            <filter-class>org.jasig.cas.client.util.AssertionThreadLocalFilter</filter-class>
        </filter>
        <filter-mapping>
            <filter-name>CAS Assertion Thread Local Filter</filter-name>
            <url-pattern>/*</url-pattern>
        </filter-mapping>
        <!-- ======================== 单点登录结束 ======================== -->
    

    测试SSO功能

    至此,所以配置都已经配置完成。这时候需要测试单点登录功能:

    首先,重启 tomcat client 和 tomcat client2。也就是说,在这个时候,我们的三个tomcat都已经启动成功,分别为:CAS Server 、 client1 、client2。

    然后,访问:http://client1.castest.com:18080/examples/servlets/servlet/HelloWorldExample

    这时候,会发现浏览器地址重定向到了:

    https://sso.castest.com:8443/cas/login?service=http%3A%2F%2Fclient1.castest.com%3A18080%2Fexamples%2Fservlets%2Fservlet%2FHelloWorldExample

    不过后面那一串是什么东西?利用在线工具对这个地址解码,得到以下地址:
    https://sso.castest.com:8443/cas/login?service=http://client1.castest.com:18080/examples/servlets/servlet/HelloWorldExample

    是不是很熟悉,现在这个过程,是不是很像 “CAS SSO 原理”里面的内容呢?也就是说,在我们访问client1的时候,按CAS 认证流程走了一遍。后面那个参数:service=xxx,就是我们认证成功之后重定向的地址,也就是我们当前访问的地址。

    这时候输入用户名Moxie 密码 Admin,发现界面出现了 我想想要访问的资源,同时,观察浏览器地址栏的变化:

    这时候我们再访问 client2,因我我们刚刚已经登录过一次了,所以,这次在我们访问client2的时候,按理来说是不需要重新登录的,下面进行测试:

    在浏览器地址栏输入:

    http://client2.castest.com:28080/examples/servlets/servlet/HelloWorldExample

    然而,效果却并不是我们预期的那样,竟然出现以下界面:

    这是什么原因?经过一番google,发现原来是:CAS服务器的ST票据有效期时间太短,默认是10秒。开发在debug时,非常容易超过10秒,所以会发生TicketValidationException异常。
    首先,设置CAS Server的超时时间:
    编辑D:\GreenSoft\Apach\Tomcat\Tomcat-8.0.30-x64\webapps\cas\WEB-INF\cas.properties
    设置有效时间为: 3600

    重启CAS Server, 并且登出。
    http://client1.castest.com:18080/examples/servlets/servlet/HelloWorldExample
    提示输入用户名密码,我们输入 Moxie Admin 进行登录,出现以下界面;

    说明client1没有问题。

    然后访问client2
    http://client2.castest.com:28080/examples/servlets/servlet/HelloWorldExample
    同样出现以下界面

    spring-security集成,主要介绍spring-security-cas的使用。

    根据上面的几篇文章,可以知道CAS分为Client端和Server端,这里主要讲的是Client端。

    casSecurity.xml

    <!-- 此文件用于CAS sso登录方式 -->
    <beans:beans xmlns="http://www.springframework.org/schema/security"
                 xmlns:beans="http://www.springframework.org/schema/beans"
                 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                 xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
                                     http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-4.0.xsd">
        <!-- CAS 基本属性配置-->
        <beans:bean id="serviceProperties"
                    class="org.springframework.security.cas.ServiceProperties">
            <beans:property name="service"
                            value="${cas.service}"/>
            <beans:property name="sendRenew" value="false"/>
        </beans:bean>
        <!-- CAS 认证配置 -->
        <http entry-point-ref="casEntryPoint" access-decision-manager-ref="accessDecisionManager">
            <csrf request-matcher-ref="csrfCasSecurityRequestMatcher"/>
            <intercept-url pattern="/resources/**" access="permitAll"/>
            <intercept-url pattern="/lib/**" access="permitAll"/>
      <!--      <intercept-url pattern="/common/**" access="permitAll"/>-->
            <!-- <intercept-url pattern="/login" access="permitAll" />
             <intercept-url pattern="/login.html" access="permitAll" />
             <intercept-url pattern="/verifiCode" access="permitAll" />-->
            <intercept-url pattern="/timeout" access="permitAll"/>
            <intercept-url pattern="/websocket/**" access="permitAll"/>
            <intercept-url pattern="/**" access="hasRole('ROLE_USER')"/>
            <!--   <access-denied-handler error-page="/access-denied"/>
               <session-management invalid-session-url="/timeout"/>-->
            <!--<form-login login-page='/login' authentication-success-handler-ref="successHandler"
                        authentication-failure-handler-ref="loginFailureHandler"/>-->
            <!--authentication-failure-url="/login?error=true"/>-->
            <!-- 验证码拦截器 -->
            <!--  <custom-filter ref="captchaVerifierFilter" before="FORM_LOGIN_FILTER"/>-->
            <custom-filter ref="requestSingleLogoutFilter" before="LOGOUT_FILTER"/>
            <custom-filter position="CAS_FILTER" ref="casFilter"/>
            <custom-filter ref="singleLogoutFilter" before="CAS_FILTER"/>
            <headers defaults-disabled="true">
                <cache-control/>
            </headers>
        </http>
        <!-- 认证管理器,确定用户,角色及相应的权限 -->
        <beans:bean id="accessDecisionManager" class="org.springframework.security.access.vote.UnanimousBased">
            <!-- 投票器 -->
            <beans:constructor-arg>
                <beans:list>
                    <beans:bean class="com.hand.hap.security.PermissionVoter"/>
                    <beans:bean class="org.springframework.security.web.access.expression.WebExpressionVoter"/>
                    <beans:bean class="org.springframework.security.access.vote.RoleVoter"/>
                    <beans:bean class="org.springframework.security.access.vote.AuthenticatedVoter"/>
                </beans:list>
            </beans:constructor-arg>
        </beans:bean>
        <!-- CAS Filter 配置 -->
        <beans:bean id="casFilter"
                    class="org.springframework.security.cas.web.CasAuthenticationFilter">
            <beans:property name="authenticationManager" ref="authenticationManager"/>
            <beans:property name="authenticationSuccessHandler" ref="successHandler"/>
        </beans:bean>
        <beans:bean id="successHandler" class="com.hand.hap.security.CustomAuthenticationSuccessHandler">
            <beans:property name="defaultTargetUrl" value="/index"/>
        </beans:bean>
        <beans:bean id="casEntryPoint"
                    class="org.springframework.security.cas.web.CasAuthenticationEntryPoint">
            <beans:property name="loginUrl" value="${cas.ssoserver.loginurl}"/>
            <beans:property name="serviceProperties" ref="serviceProperties"/>
        </beans:bean>
        <authentication-manager alias="authenticationManager">
            <authentication-provider ref="casAuthenticationProvider"/>
            <!-- <authentication-provider user-service-ref="customUserDetailsService">
                 <password-encoder ref="passwordManager"/>
             </authentication-provider>-->
        </authentication-manager>
        <beans:bean id="casAuthenticationProvider"
                    class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
            <beans:property name="serviceProperties" ref="serviceProperties"/>
            <beans:property name="authenticationUserDetailsService" ref="customAuthenticationUserDetailsService"/>
            <beans:property name="ticketValidator">
                <beans:bean class="org.jasig.cas.client.validation.Cas20ServiceTicketValidator">
                    <beans:constructor-arg index="0" value="${cas.ssoserver.url}"/>
                </beans:bean>
            </beans:property>
            <beans:property name="key" value="an_id_for_this_auth_provider_only"/>
        </beans:bean>
        <beans:bean id="customAuthenticationUserDetailsService"
                    class="com.hand.hap.security.CustomAuthenticationUserDetailsService">
        </beans:bean>
        <beans:bean id="singleLogoutFilter" class="org.jasig.cas.client.session.SingleSignOutFilter"/>
        <!-- This filter redirects to the CAS Server to signal Single Logout should be performed -->
        <beans:bean id="requestSingleLogoutFilter"
                    class="org.springframework.security.web.authentication.logout.LogoutFilter">
            <beans:constructor-arg value="${cas.ssoserver.logouturl}"/>
            <beans:constructor-arg>
                <beans:bean class="org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler"/>
            </beans:constructor-arg>
            <beans:property name="filterProcessesUrl" value="/logout"/>
        </beans:bean>
        <beans:bean id="csrfCasSecurityRequestMatcher" class="com.hand.hap.security.CsrfSecurityRequestMatcher">
            <beans:property name="excludeUrls">
                <beans:list>
                    <beans:value>/login</beans:value>
                    <beans:value>/websocket/**</beans:value>
                    <beans:value>/ureport/**</beans:value>
                </beans:list>
            </beans:property>
        </beans:bean>
        <!-- <beans:bean id="captchaVerifierFilter" class="com.hand.hap.security.CaptchaVerifierFilter">
             <beans:property name="captchaField" value="verifiCode"/>
         </beans:bean>
         <beans:bean id="loginFailureHandler" class="com.hand.hap.security.LoginFailureHandler"/>-->
    </beans:beans>
    

    config.properties

    cas.service=http://localhost:8080/hap/login/cas
    cas.ssoserver.loginurl=https://localhost:8088/sso/login
    cas.ssoserver.url=https://localhost:8088/sso
    cas.ssoserver.logouturl=https://localhost:8088/sso/logout?service=http://localhost:8080/hap
    

    在Spring Security中,通过设置entry-point-ref="第三方登录入口",可以在访问系统首页的时候进行登录跳转。它在系统进行登录认证的过程会进行认证,认证不通过则抛出一个异常给ExceptionTranslationFilter,由它进行通过entry-point-ref设置的入口点进行处理。

     <custom-filter position="CAS_FILTER" ref="casFilter"/>
    

    position="CAS_FILTER"就表示将定义的 Filter 放在 CAS_FILTER 对应的那个位置,这里对应的是 casFilter

        <!-- CAS Filter 配置 -->
        <beans:bean id="casFilter"
                    class="org.springframework.security.cas.web.CasAuthenticationFilter">
            <beans:property name="authenticationManager" ref="authenticationManager"/>
            <beans:property name="authenticationSuccessHandler" ref="successHandler"/>
        </beans:bean>
        <!-- 登录成功处理 -->
        <beans:bean id="successHandler" class="com.hand.hap.security.CustomAuthenticationSuccessHandler">
            <beans:property name="defaultTargetUrl" value="/index"/>
        </beans:bean>
    

    其实在登录时的权限校验和之前的一样,区别在于

  • AuthenticationProvider 用的是 CasAuthenticationProvider
  • 使用 entry-point-ref="casEntryPoint" 来处理登录失败时的一些逻辑
  • CasAuthenticationEntryPoint 中的 commence 方法如下,重定向到 redirectUrl,这些就是我们在使用 spring-security-cas 需要的配置信息:

        public final void commence(final HttpServletRequest servletRequest,
                final HttpServletResponse response,
                final AuthenticationException authenticationException) throws IOException,
                ServletException {
            final String urlEncodedService = createServiceUrl(servletRequest, response);
            final String redirectUrl = createRedirectUrl(urlEncodedService);