使用 CAS 在 Tomcat 中实现单点登录CAS Client 与受保护的客户端应用部署在一起,以 Filter 方式保护受保护
使用 CAS 在 Tomcat 中实现单点登录
CAS Client 与受保护的客户端应用部署在一起,以 Filter 方式保护受保护的资源。对于访问受保护资源的每个 Web 请求,CAS Client 会分析该请求的 Http 请求中是否包含 Service Ticket,如果没有,则说明当前用户尚未登录,于是将请求重定向到指定好的 CAS Server 登录地址,并传递 Service (也就是要访问的目的资源地址),以便登录成功过后转回该地址。用户在第 3 步中输入认证信息,如果登录成功,CAS Server 随机产生一个相当长度、唯一、不可伪造的 Service Ticket,并缓存以待将来验证,之后系统自动重定向到 Service 所在地址,并为客户端浏览器设置一个 Ticket Granted Cookie(TGC),CAS Client 在拿到 Service 和新产生的 Ticket 过后,在第 5,6 步中与 CAS Server 进行身份合适,以确保 Service Ticket 的合法性。
在该协议中,所有与 CAS 的交互均采用 SSL 协议,确保,ST 和 TGC 的安全性。协议工作过程中会有 2 次重定向的过程,但是 CAS Client 与 CAS Server 之间进行 Ticket 验证的过程对于用户是透明的。
另外,CAS 协议中还提供了 Proxy (代理)模式,以适应更加高级、复杂的应用场景,具体介绍可以参考 CAS 官方网站上的相关文档。
回页首
[java]
public?abstract?class?AbstractUsernamePasswordAuthenticationHandler?extends??????????????????????????AbstractPreAndPostProcessingAuthenticationHandler{??...???protected?final?boolean?doAuthentication(final?Credentials?credentials)???throws?AuthenticationException?{???return?authenticateUsernamePasswordInternal((UsernamePasswordCredentials)?credentials);???}???protected?abstract?boolean?authenticateUsernamePasswordInternal(??????????final?UsernamePasswordCredentials?credentials)?throws?AuthenticationException;?????protected?final?PasswordEncoder?getPasswordEncoder()?{???return?this.passwordEncoder;???}??public?final?void?setPasswordEncoder(final?PasswordEncoder?passwordEncoder)?{???this.passwordEncoder?=?passwordEncoder;??????}??...??}??
基于用户名密码的认证方式可直接扩展自 AbstractUsernamePasswordAuthenticationHandler,验证用户名密码的具体操作通过实现 authenticateUsernamePasswordInternal() 方法达到,另外,通常情况下密码会是加密过的,setPasswordEncoder() 方法就是用于指定适当的加密器。
从以上清单中可以看到,doAuthentication() 方法的参数是 Credentials 类型,这是包含用户认证信息的一个接口,对于用户名密码类型的认证信息,可以直接使用 UsernamePasswordCredentials,如果需要扩展其他类型的认证信息,需要实现Credentials接口,并且实现相应的 CredentialsToPrincipalResolver 接口,其具体方法可以借鉴 UsernamePasswordCredentials 和 UsernamePasswordCredentialsToPrincipalResolver。
JDBC 认证方法
用户的认证信息通常保存在数据库中,因此本文就选用这种情况来介绍。将前面下载的 cas-server-3.1.1-release.zip 包解开后,在 modules 目录下可以找到包 cas-server-support-jdbc-3.1.1.jar,其提供了通过 JDBC 连接数据库进行验证的缺省实现,基于该包的支持,我们只需要做一些配置工作即可实现 JDBC 认证。
JDBC 认证方法支持多种数据库,DB2, Oracle, MySql, Microsoft SQL Server 等均可,这里以 DB2 作为例子介绍。并且假设DB2数据库名: CASTest,数据库登录用户名: db2user,数据库登录密码: db2password,用户信息表为: userTable,该表包含用户名和密码的两个数据项分别为 userName 和 password。
1. 配置 DataStore
打开文件 %CATALINA_HOME%/webapps/cas/WEB-INF/deployerConfigContext.xml,添加一个新的 bean 标签,对于 DB2,内容如清单 4 所示:
清单 4. 配置 DataStore
[html]<bean?id="casDataSource"?class="org.apache.commons.dbcp.BasicDataSource">???????<property?name="driverClassName">????????????<value>com.ibm.db2.jcc.DB2Driver</value>???????</property>???????<property?name="url">????????????<value>jdbc:db2://9.125.65.134:50000/CASTest</value>???????</property>???????<property?name="username">????????????<value>db2user</value>???????</property>???????<property?name="password">????????????<value>db2password</value>???????</property>??</bean>???
?
?
?
其中 id 属性为该 DataStore 的标识,在后面配置 AuthenticationHandler 会被引用,另外,需要提供 DataStore 所必需的数据库驱动程序、连接地址、数据库登录用户名以及登录密码。
2. 配置 AuthenticationHandler
在 cas-server-support-jdbc-3.1.1.jar 包中,提供了 3 个基于 JDBC 的 AuthenticationHandler,分别为 BindModeSearchDatabaseAuthenticationHandler, QueryDatabaseAuthenticationHandler, SearchModeSearchDatabaseAuthenticationHandler。其中 BindModeSearchDatabaseAuthenticationHandler 是用所给的用户名和密码去建立数据库连接,根据连接建立是否成功来判断验证成功与否;QueryDatabaseAuthenticationHandler 通过配置一个 SQL 语句查出密码,与所给密码匹配;SearchModeSearchDatabaseAuthenticationHandler 通过配置存放用户验证信息的表、用户名字段和密码字段,构造查询语句来验证。
使用哪个 AuthenticationHandler,需要在 deployerConfigContext.xml 中设置,默认情况下,CAS 使用一个简单的 username=password 的 AuthenticationHandler,在文件中可以找到如下一行:<bean />,我们可以将其注释掉,换成我们希望的一个 AuthenticationHandler,比如,使用QueryDatabaseAuthenticationHandler 或 SearchModeSearchDatabaseAuthenticationHandler 可以分别选取清单 5 或清单 6 的配置。
清单 5. 使用 QueryDatabaseAuthenticationHandler
[html]??????????????????<bean?class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler">???<property?name="dataSource"?ref="?casDataSource?"?/>???<property?name="sql"??????????value="select?password?from?userTable?where?lower(userName)?=?lower(?)"?/>??</bean>??
清单 6. 使用 SearchModeSearchDatabaseAuthenticationHandler
[html]<bean?id="SearchModeSearchDatabaseAuthenticationHandler"????????class="org.jasig.cas.adaptors.jdbc.SearchModeSearchDatabaseAuthenticationHandler"????????abstract="false"?singleton="true"?lazy-init="default"??????????????????????????autowire="default"?dependency-check="default">????<property??name="tableUsers">?????<value>userTable</value>????</property>????<property?name="fieldUser">?????<value>userName</value>????</property>????<property?name="fieldPassword">?????<value>password</value>????</property>????<property?name="dataSource"?ref="?casDataSource?"?/>??</bean>??
?
?
另外,由于存放在数据库中的密码通常是加密过的,所以 AuthenticationHandler 在匹配时需要知道使用的加密方法,在 deployerConfigContext.xml 文件中我们可以为具体的 AuthenticationHandler 类配置一个 property,指定加密器类,比如对于 QueryDatabaseAuthenticationHandler,可以修改如清单7所示:
清单 7. 添加 passwordEncoder
[html]<bean?class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler">????<property?name="dataSource"?ref="?casDataSource?"?/>????<property?name="sql"??????????????value="select?password?from?userTable?where?lower(userName)?=?lower(?)"?/>????<property??name="passwordEncoder"??ref="myPasswordEncoder"/>??</bean>??
其中 myPasswordEncoder 是对清单 8 中设置的实际加密器类的引用:
清单 8. 指定具体加密器类
[html]<bean?id="passwordEncoder"???????????????class="org.jasig.cas.authentication.handler.MyPasswordEncoder"/>??
这里 MyPasswordEncoder 是根据实际情况自己定义的加密器,实现 PasswordEncoder 接口及其 encode() 方法。
3. 部署依赖包
在以上配置完成以后,需要拷贝几个依赖的包到 cas 应用下,包括:
将 cas-server-support-jdbc-3.1.1.jar 拷贝到 %CATALINA_HOME%/webapps/cas/ WEB-INF/lib 目录。数据库驱动,由于这里使用 DB2,将 %DB2_HOME%/java 目录下的 db2java.zip (更名为 db2java.jar), db2jcc.jar, db2jcc_license_cu.jar 拷贝到 %CATALINA_HOME%/webapps/cas/WEB-INF/lib 目录。对于其他数据库,同样将相应数据库驱动程序拷贝到该目录。DataStore 依赖于 commons-collections-3.2.jar, commons-dbcp-1.2.1.jar, commons-pool-1.3.jar,需要到 apache 网站的 Commons 项目下载以上 3 个包放进 %CATALINA_HOME%/webapps/cas/WEB-INF/lib 目录。[html]
<bean?id="viewResolver"????????class="org.springframework.web.servlet.view.ResourceBundleViewResolver"?p:order="0">??????<property?name="basenames">??????????<list>??????????????<value>${cas.viewResolver.basename}</value>??????????????<value>?newUI_views</value>??????????</list>??????</property>??</bean>??
回页首
[html]
<web-app>????...????<filter>??????<filter-name>CAS?Filter</filter-name>??????<filter-class>edu.yale.its.tp.cas.client.filter.CASFilter</filter-class>??????<init-param>????????<param-name>edu.yale.its.tp.cas.client.filter.loginUrl</param-name>????????<param-value>https://domainA:8443/cas/login</param-value>??????</init-param>??????<init-param>????????<param-name>edu.yale.its.tp.cas.client.filter.validateUrl</param-name>????????<param-value>https://domainA:8443/cas/serviceValidate</param-value>??????</init-param>??????<init-param>????????<param-name>edu.yale.its.tp.cas.client.filter.serverName</param-name>????????<param-value>domainB:8080</param-value>??????</init-param>????</filter>????<filter-mapping>??????<filter-name>CAS?Filter</filter-name>??????<url-pattern>/protected-pattern/*</url-pattern>????</filter-mapping>????...??</web-app>??
对于所有访问满足 casTest1/protected-pattern/ 路径的资源时,都要求到 CAS Server 登录,如果需要整个 casTest1 均受保护,可以将 url-pattern 指定为“/*”。
从清单 10 可以看到,我们可以为 CASFilter 指定一些参数,并且有些是必须的,表格 1[java]
//?以下两者都可以??session.getAttribute(CASFilter.CAS_FILTER_USER);??session.getAttribute("edu.yale.its.tp.cas.client.filter.user");??
在 JSTL 中获取用户名的方法如清单 12 所示:
清单 12. 通过 JSTL 获取登录用户名
[java]CASFilterRequestWrapper??reqWrapper=new?CASFilterRequestWrapper(request);??out.println("The?logon?user:"?+?reqWrapper.getRemoteUser());??
回页首
[java]
public?class?WelcomePage?extends?HttpServlet?{????public?void?doGet(HttpServletRequest?request,?HttpServletResponse?response)????throws?IOException,?ServletException????{??????response.setContentType("text/html");??????PrintWriter?out?=?response.getWriter();??????out.println("<html>");??????out.println("<head>");??????out.println("<title>Welcome?to?casTest2?sample?System!</title>");??????out.println("</head>");??????out.println("<body>");??????out.println("<h1>Welcome?to?casTest1?sample?System!</h1>");??????CASFilterRequestWrapper??reqWrapper=new?CASFilterRequestWrapper(request);??????out.println("<p>The?logon?user:"?+?reqWrapper.getRemoteUser()?+?"</p>");??????HttpSession?session=request.getSession();??????out.println("<p>The?logon?user:"?+??????????????????????session.getAttribute(CASFilter.CAS_FILTER_USER)??+?"</p>");??????out.println("<p>The?logon?user:"?+????????????session.getAttribute("edu.yale.its.tp.cas.client.filter.user")?+?"</p>");??????out.println("</body>");??????out.println("</html>");??????}??}??
在上面所有配置结束过后,分别在 A, B, C上启动 cas, casTest1 和 casTest2,按照下面步骤来访问 casTest1 和 casTest2:
打开浏览器,访问登录成功后,再重定向到 casTest1 的 WelcomePage 页面,如图所示:
图 3. 登录后访问 casTest1 的效果

可以看到图中地址栏里的地址多出了一个 ticket 参数,这就是 CAS 分配给当前应用的 ST(Service Ticket)。
再在同一个浏览器的地址栏中输入重新打开一个浏览器窗口,先输入?http://domainC:8080/casTest2/WelcomePage?,系统要求登录,在登录成功过后,正确显示 casTest2 的页面。之后再在地址栏重新输入http://domainB:8080/casTest1/WelcomePage?,会直接显示 casTest1 的页面而无需再次登录
http://blog.csdn.net/gs250220/article/details/7107503