首页 诗词 字典 板报 句子 名言 友答 励志 学校 网站地图
当前位置: 首页 > 教程频道 > 软件管理 > 软件架构设计 >

运用 CAS 在 Tomcat 中实现单点登录

2013-09-11 
使用 CAS 在 Tomcat 中实现单点登录一CAS 是 Yale 大学发起的一个开源项目,旨在为 Web 应用系统提供一种可

使用 CAS 在 Tomcat 中实现单点登录

CAS 是 Yale 大学发起的一个开源项目,旨在为 Web 应用系统提供一种可靠的单点登录方法,CAS 在 2004 年 12 月正式成为 JA-SIG 的一个项目。CAS 具有以下特点:
1.开源的企业级单点登录解决方案。
2.CAS Server 为需要独立部署的 Web 应用。
3.CAS Client 支持非常多的客户端(这里指单点登录系统中的各个 Web 应用),包括 Java, .Net, PHP, Perl, Apache, uPortal, Ruby 等。

二、CAS 原理和协议
从结构上看,CAS 包含两个部分: CAS Server 和 CAS Client。CAS Server 需要独立部署,主要负责对用户的认证工作;CAS Client 负责处理对客户端受保护资源的访问请求,需要登录时,重定向到 CAS Server。图1 是 CAS 最基本的协议过程:
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 官方网站上的相关文档。

三、准备工作
本文中的例子以 tomcat5.5 为例进行讲解,下载地址:
http://tomcat.apache.org/download-55.cgi
到 CAS 官方网站下载 CAS Server 和 Client,地址分别为:
http://www.ja-sig.org/downloads/cas/cas-server-3.1.1-release.zip
http://www.ja-sig.org/downloads/cas-clients/cas-client-java-2.1.1.zip

四、部署 CAS Server
CAS Server 是一套基于 Java 实现的服务,该服务以一个 Java Web Application 单独部署在与 servlet2.3 兼容的 Web 服务器上,另外,由于 Client 与 CAS Server 之间的交互采用 Https 协议,因此部署 CAS Server 的服务器还需要支持 SSL 协议。当 SSL 配置成功过后,像普通 Web 应用一样将 CAS Server 部署在服务器上就能正常运行了,不过,在真正使用之前,还需要扩展验证用户的接口。
4.1:在 Tomcat 上部署一个完整的 CAS Server 主要按照以下几个步骤:
配置 Tomcat 使用 Https 协议
如果希望 Tomcat 支持 Https,主要的工作是配置 SSL 协议,其配置过程和配置方法可以参考 Tomcat 的相关文档。不过在生成证书的过程中,会有需要用到主机名的地方,CAS 建议不要使用 IP 地址,而要使用机器名或域名。
参见Tomcat6.0配置SSL: http://danwind.iteye.com/admin/blogs/603402
(注:keytool -genkey -alias tomcat -keyalg RSA -keypass changeit -storepass changeit -keystore server-cer.keystore -validity 3600
对于配置cas的证书:这个选项“您的名字与姓氏是什么?”不要写ip地址,建议是域名

4.2:部署 CAS Server
CAS Server 是一个 Web 应用包,将前面下载的 cas-server-3.1.1-release.zip 解开,把其中的 cas-server-webapp-3.1.1.war 拷贝到 tomcat的 webapps 目录,并更名为 cas.war。由于前面已配置好 tomcat 的 https 协议,可以重新启动 tomcat,然后访问:https://localhost:8443/cas ,如果能出现正常的 CAS 登录页面,则说明 CAS Server 已经部署成功。
虽然 CAS Server 已经部署成功,但这只是一个缺省的实现,在实际使用的时候,还需要根据实际概况做扩展和定制,最主要的是扩展认证 (Authentication) 接口和 CAS Server 的界面。
4.3:扩展认证接口
CAS Server 负责完成对用户的认证工作,它会处理登录时的用户凭证 (Credentials) 信息,用户名/密码对是最常见的凭证信息。CAS Server 可能需要到数据库检索一条用户帐号信息,也可能在 XML 文件中检索用户名/密码,还可能通过 LDAP Server 获取等,在这种情况下,CAS 提供了一种灵活但统一的接口和实现分离的方式,实际使用中 CAS 采用哪种方式认证是与 CAS 的基本协议分离开的,用户可以根据认证的接口去定制和扩展。
4.4:扩展 AuthenticationHandler
CAS 提供扩展认证的核心是 AuthenticationHandler 接口,该接口定义如清单 1 下:
清单 1. AuthenticationHandler定义               

public interface AuthenticationHandler {    /**     * Method to determine if the credentials supplied are valid.     * @param credentials The credentials to validate.     * @return true if valid, return false otherwise.     * @throws AuthenticationException An AuthenticationException can contain     * details about why a particular authentication request failed.     */    boolean authenticate(Credentials credentials) throws AuthenticationException;/**     * Method to check if the handler knows how to handle the credentials     * provided. It may be a simple check of the Credentials class or something     * more complicated such as scanning the information contained in the     * Credentials object.      * @param credentials The credentials to check.     * @return true if the handler supports the Credentials, false othewrise.     */    boolean supports(Credentials credentials);}

该接口定义了 2 个需要实现的方法,supports ()方法用于检查所给的包含认证信息的Credentials 是否受当前 AuthenticationHandler 支持;而 authenticate() 方法则担当验证认证信息的任务,这也是需要扩展的主要方法,根据情况与存储合法认证信息的介质进行交互,返回 boolean 类型的值,true 表示验证通过,false 表示验证失败。
CAS3中还提供了对AuthenticationHandler 接口的一些抽象实现,比如,可能需要在执行authenticate() 方法前后执行某些其他操作,那么可以让自己的认证类扩展自清单 2 中的抽象类:
清单 2. AbstractPreAndPostProcessingAuthenticationHandler定义               
public abstract class AbstractPreAndPostProcessingAuthenticationHandler                                            implements AuthenticateHandler{    protected Log log = LogFactory.getLog(this.getClass());    protected boolean preAuthenticate(final Credentials credentials) {        return true;    }    protected boolean postAuthenticate(final Credentials credentials,        final boolean authenticated) {        return authenticated;    }    public final boolean authenticate(final Credentials credentials)        throws AuthenticationException {        if (!preAuthenticate(credentials)) {            return false;        }        final boolean authenticated = doAuthentication(credentials);        return postAuthenticate(credentials, authenticated);    }    protected abstract boolean doAuthentication(final Credentials credentials) throws AuthenticationException;}

AbstractPreAndPostProcessingAuthenticationHandler 类新定义了 preAuthenticate() 方法和 postAuthenticate() 方法,而实际的认证工作交由 doAuthentication() 方法来执行。因此,如果需要在认证前后执行一些额外的操作,可以分别扩展 preAuthenticate()和 ppstAuthenticate() 方法,而 doAuthentication() 取代 authenticate() 成为了子类必须要实现的方法。
由于实际运用中,最常用的是用户名和密码方式的认证,CAS3 提供了针对该方式的实现,如清单 3 所示:
清单 3. AbstractUsernamePasswordAuthenticationHandler 定义               
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。
5.1: 配置 DataStore
打开文件 %CATALINA_HOME%/webapps/cas/WEB-INF/deployerConfigContext.xml,添加一个新的 bean 标签,对于 DB2,内容如清单 4 所示:
清单 4. 配置 DataStore
 <bean id="casDataSource" />,我们可以将其注释掉,换成我们希望的一个 AuthenticationHandler,比如,使用QueryDatabaseAuthenticationHandler 或 SearchModeSearchDatabaseAuthenticationHandler 可以分别选取清单 5 或清单 6 的配置。
清单 5. 使用 QueryDatabaseAuthenticationHandler               
<bean ref=" casDataSource " /> <property name="sql"        value="select password from userTable where lower(userName) = lower(?)" /></bean>

清单 6. 使用 SearchModeSearchDatabaseAuthenticationHandler               
<bean id="SearchModeSearchDatabaseAuthenticationHandler"      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    
<bean ref=" casDataSource " />  <property name="sql"            value="select password from userTable where lower(userName) = lower(?)" />  <property  name="passwordEncoder"  ref="myPasswordEncoder"/></bean>

清单 8. 指定具体加密器类               
<bean id="passwordEncoder"             name="code"><bean id="viewResolver"      p:order="0">    <property name="basenames">        <list>            <value>${cas.viewResolver.basename}</value>            <value> newUI_views</value>        </list>    </property></bean>

六、部署客户端应用
单点登录的目的是为了让多个相关联的应用使用相同的登录过程,本文在讲解过程中构造 2个简单的应用,分别以 casTest1 和 casTest2 来作为示例,它们均只有一个页面,显示欢迎信息和当前登录用户名。这 2 个应用使用同一套登录信息,并且只有登录过的用户才能访问,通过本文的配置,实现单点登录,即只需登录一次就可以访问这两个应用。
6.1:与 CAS Server 建立信任关系
假设 CAS Server 单独部署在一台机器 A,而客户端应用部署在机器 B 上,由于客户端应用与 CAS Server 的通信采用 SSL,因此,需要在 A 与 B 的 JRE 之间建立信任关系。
首先与 A 机器一样,要生成 B 机器上的证书,配置 Tomcat 的 SSL 协议。其次,下载http://blogs.sun.com/andreas/entry/no_more_unable_to_find 的 InstallCert.java,运行“ java InstallCert compA:8443 ”命令,并且在接下来出现的询问中输入 1。这样,就将 A 添加到了 B 的 trust store 中。如果多个客户端应用分别部署在不同机器上,那么每个机器都需要与 CAS Server 所在机器建立信任关系。
6.2:配置 CAS Filter
准备好应用 casTest1 和 casTest2 过后,分别部署在 B 和 C 机器上,由于 casTest1 和casTest2,B 和 C 完全等同,我们以 casTest1 在 B 机器上的配置做介绍,假设 A 和 B 的域名分别为 domainA 和 domainB。
将 cas-client-java-2.1.1.zip 改名为 cas-client-java-2.1.1.jar 并拷贝到 casTest1/WEB-INF/lib目录下,修改 web.xml 文件,添加 CAS Filter,如清单 10 所示:
清单 10. 添加 CAS Filter
                <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 和表格 2 中分别是必需和可选的参数:
表格 1. CASFilter 必需的参数
// 以下两者都可以session.getAttribute(CASFilter.CAS_FILTER_USER);session.getAttribute("edu.yale.its.tp.cas.client.filter.user");

在 JSTL 中获取用户名的方法如清单 12 所示:
清单 12. 通过 JSTL 获取登录用户名               
<c:out value="${sessionScope[CAS:'edu.yale.its.tp.cas.client.filter.user']}"/>

另外,CAS 提供了一个 CASFilterRequestWrapper 类,该类继承自HttpServletRequestWrapper,主要是重写了 getRemoteUser() 方法,只要在前面配置 CASFilter 的时候为其设置“ edu.yale.its.tp.cas.client.filter.wrapRequest ”参数为 true,就可以通过 getRemoteUser() 方法来获取登录用户名,具体方法如清单 13 所示:
清单 13. 通过 CASFilterRequestWrapper 获取登录用户名               
CASFilterRequestWrapper  reqWrapper=new CASFilterRequestWrapper(request);out.println("The logon user:" + reqWrapper.getRemoteUser());

6.4:效果
在 casTest1 和 casTest2 中,都有一个简单 Servlet 作为欢迎页面 WelcomPage,且该页面必须登录过后才能访问,页面代码如清单 14 所示:
清单 14. WelcomePage 页面代码              
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:
结束语

本文介绍了 CAS 单点登录解决方案的原理,并结合实例讲解了在 Tomcat 中使用 CAS 的配置、部署方法以及效果。CAS 是作为开源单点登录解决方案的一个不错选择,更多的使用细节可以参考 CAS 官方网站。

七、测试过程中的异常
7.1:sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
keytool -import -file server.crt -keystore %java_home%/jre/lib/security/cacerts的时候一直是往jdk1.5的jre添加证书。而Tomcat用的jre一直没有添加证书,所以一直有unable to find valid certification path to requested target的异常。往eclipse的jre添加证书之后,CAS终于运行正常。!!!!!!!!
总结:不仅仅这个问题,其实有很多问题都是因为系统装了多个各个版本的JDK而引起的。最好只保留一个JDK,可以避免很多的稀奇古怪的问题。否则,一定要仔细确认tomcat等其他依赖JDk的应用程序到底用的是哪个JDK。。
7.2:java.lang.ClassNotFoundException: edu.yale.its.tp.cas.client.filter.CASFilter
加载cas的jar包
7.3:java.security.cert.CertificateException: No subject alternative names present
这个是由于CAS 建议不要使用 IP 地址,而要使用机器名或域名。出现这个问题时,更换ip地址为域名(在这证书的名称及cas域名配置两处地方)
7.4:java.lang.IllegalArgumentException: setAttribute: Non-serializable attribute
这个是由于web.xmld文件中的出现了“<distributable />”,删除它即可。

八、解释CAS Logout问题
假设有webapp1, webapp2, cas server,webapp1, webapp2均受cas server保护
首先,我在这里简单解释一下:
第1种不能logout的情况:
1)登录了WebApp1,redirect到caserver
casserver认证后,再redirect到webapp1,ok!
2)http方式 lougout casserver1,即http://yale_casserver:8080/cas/lougout
显示logout成功
3)访问webapp2,还能访问!这是非常正常的一种情况,因为你不通过https来注销,casserver怎么"杀"掉
它通过https发给你的TGC Cookie?
解决办法:
  调用cas应用的路径进行推出:
response.sendRedirect("https://:8443/cas/logout");
第2种不能logout的情况:
1)登录了WebApp1,redirect到caserver
casserver认证后,再redirect到webapp1,ok!
2)https方式 lougout casserver1,即https://yale_casserver:8443/cas/lougout
显示logout成功
3)访问webapp1,还能访问!访问webapp2,不能访问,重定向到casserver要求登录!这也是非常正常的一种情况,因为你已经能够访问,你继续可以继续访问,
CASLogout不能阻止你访问webapp1,它只能阻止你访问webapp2,因为你已经
被允许访问webapp1,而webapp2则还没有,如果你在(1)的时候,顺带也访问
webapp2,那么你的注销将毫无作用了,CAS无法阻止你访问这两个webapp,
因为你有Service Ticket。
如果你对此费解,那时因为你已为Logout就是退出系统,那我只能表示遗憾,
因为CAS Logout的作用不是这样,它的作用是阻止你继续通过TGC(它简单地
清楚了IE的TGC Cookie)来获取ST,阻止你获取通向其他web应用的Ticket。
所以,用完webapp1的时候,注销,然后再关闭掉IE就彻底Logout了

解决办法:
先进行去除以下两个session的属性:
  HttpSession session = request.getSession();
        if(session.getAttribute("edu.yale.its.tp.cas.client.filter.user") !=null ) {
        session.setAttribute("edu.yale.its.tp.cas.client.filter.user", "");
       
        }
        if(session.getAttribute("edu.yale.its.tp.cas.client.filter.receipt") !=null ) {
        session.setAttribute("edu.yale.its.tp.cas.client.filter.receipt", null);
       
        }
然后再进行去除以下用户session的id:
request.removeAttribute("user.id");
最后通过https来注销:response.sendRedirect("https://:8443/cas/logout");
1 楼 lishouxinghome 2011-08-05   请问如何获得用户的Id呢,往指点
我的异常网推荐解决方案:java.lang.ClassNotFoundException: org.apache.commons.dbcp.BasicDataSource,http://www.myexception.cn/j2ee/182233.html

热点排行