一.开发准备:
环境:
OS:windows xp
IDE:myeclipse 6.0
web服务器:tomcat 6.0
JDK: jdk1.6.0_05
数据库:mysql 6.0
准备:
cas-client-java-2.1.1.zip
http://www.ja-sig.org/downloads/cas-clients/ cas-client-java-2.1.1.zip [建议使用迅雷下载]
cas-server-3.3.1-release.zip
http://www.ja-sig.org/downloads/cas/ cas- server-3.3 .1- release .zip [建议使用迅雷下载]
其他所需jar文件:
/Files/arix04/need.part1.rar
/Files/arix04/need.part2.rar
二.开发步骤:
1.修改本机域名:
大家都知道使用 http://localhost:8080/ 就可以访问本机web服务,这里的localhost其实就是一个ip,域名映射,我们可以设置自己的本机域名,
在C:\WINDOWS\system32\drivers\etc下面找到hosts文件,用文本打开,加上一行:
127.0.0.1 http://www.test.com/
这样修改后,我们启动tomcat服务后,就可以使用 http://www.test.com:8080/ 来访问了。
这步是为了写cookie做准备,使用localhost是无法写入cookie的,不过这个demo中似乎不该也行,看个人吧,因为我写另外一个sso例子的时候这里需要修改域名,所以就把这步写一下。
2.证书生成及导入,使用jdk自带的keytool工具来生成证书
首先打开cmd,cd到自己的%JAVA_HOME%\jre\lib\security目录下
比如我的是默认目录:
C:\Program Files\Java\jdk1.6.0_05\jre\lib\security
然后执行命令(蓝色是注释不用执行):
下面2行是删除已经存在的证书,如果没有也不影响 keytool -delete -alias tomcatsso -keystore cacerts -storepass changeit keytool -delete -alias tomcatsso -storepass changeit keytool -list -keystore cacerts -storepass changeit 下面这行要注意,cn要改成你的本机域名,我的如下: keytool -genkey -keyalg RSA -alias tomcatsso -dname "cn=www.test.com" -storepass changeit 这里要求输入密码,2次输入一致就OK了 keytool -export -alias tomcatsso -file tomcatsso.crt -storepass changeit keytool -import -alias tomcatsso -file tomcatsso.crt -keystore cacerts -storepass changeit keytool -list -keystore cacerts -storepass changeit
把上面的命令都执行一遍就OK了,建议一行一行的执行,避免出错。如果成功的话,
你会在C:\Documents and Settings\当前用户[我的是C:\Documents and Settings\Administrator] 目录下发现.keystore文件,
在%JAVA_HOME%\jre\lib\security目录下发现tomcatsso.crt,如果这2个文件都存在的话,那么恭喜你,证书已经导入 成功了。
3.在tomcat中加上对ssl的支持
首先在tomcat中加上对ssl的支持,在conf/server.xml中加入下面内容:
<Connector protocol="org.apache.coyote.http11.Http11Protocol" port="8443" minSpareThreads="5" maxSpareThreads="75" enableLookups="true" disableUploadTimeout="true" acceptCount="100" maxThreads="200" scheme="https" secure="true" SSLEnabled="true" keystoreFile="C:/Documents and Settings/Administrator/.keystore" keystorePass="changeit" truststoreFile="C:/Program Files/Java/jdk1.6.0_05/jre/lib/security/cacerts" clientAuth="false" sslProtocol="TLS"/>
上面的文件路径填你自己的,密码就是刚才生 成证书时你输入的密码。
然后启动你的tomcat服务器 ,测试这个地址,
https://www.test.c om:8 443/
如果出来tomcat主页那么你的ssl就配置成功了.
4. 测试一个简单的CAS例子
到这里,我们就可以开始测试一个简单的cas单点登录的例子了,
从你的cas-server-3.3.1\modules中找出cas.war放到tomcat/webapps下面(cas-server-webapp-3.3.1.war重命名即可)。
现在cas默认的server端已经有了,下面自己写2个客户端测试一下吧
MyEclipse里面新建web project:SSO_Pro1
新建类HelloWorldExample
package servlet; import java.io.*; import java.util.*; import javax.servlet.*; import javax.servlet.http.*; /** * The simplest possible servlet. * * @author James Duncan Davidson */ public class HelloWorldExample 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>"); String title = "hello world!"; out.println("<title>" + title + "</title>"); out.println("</head>"); out.println("<body bgcolor=\"white\">"); out.println("<a href=\"../helloworld.html\">"); out.println("<img src=\"../images/code.gif\" height=24 " + "width=24 align=right border=0 alt=\"view code\"></a>"); out.println("<a href=\"../index.html\">"); out.println("<img src=\"../images/return.gif\" height=24 " + "width=24 align=right border=0 alt=\"return\"></a>"); out.println("<h1>" + title + "</h1>"); out.println("</body>"); out.println("</html>"); } } 在web.xml文件添加CASFilter与servlet映射。 Code <?xml version="1.0" encoding="GB18030"?> <web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"> <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://www.test.com:8443/cas/login</param-value> </init-param> <init-param> <param-name>edu.yale.its.tp.cas.client.filter.validateUrl</param-name> <param-value>https://www.test.com:8443/cas/serviceValidate</param-value> </init-param> <init-param> <param-name>edu.yale.its.tp.cas.client.filter.serverName</param-name> <param-value>www.test.com:8080</param-value> </init-param> </filter> <filter-mapping> <filter-name>CAS Filter</filter-name> <url-pattern>/servlet/*</url-pattern> </filter-mapping> <servlet> <servlet-name>HelloWorldExample</servlet-name> <servlet-class>servlet.HelloWorldExample</servlet-class> </servlet> <servlet-mapping> <servlet-name>HelloWorldExample</servlet-name> <url-pattern>/servlet/helloWorldExample</url-pattern> </servlet-mapping> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> </web-app>
同样,再建一个web project : SSO_Pro2
HelloWorldExample与web.xml直接从SSO_Pro1中copy即可。
OK,现在已经准备就绪,启动tomcat,然后访问:
http://www.test.com:8080/SSO_Pro1/servlet/helloWorldExample
可能会出现如下界面:
点击继续浏览此网站看看是否跳转到了CAS Server的登录界面。
如果浏览器框显示为红色,是因为该证书为非信任证书,你可以导入到信任证书即可,操作很简单,在浏览器地址栏上方按提示导入即可,则以后访问就不会出现1图。
cas服务端默认的验证机制是输入2串相同的字符串,随便输入arix04/arix04,点击登录即可。登录成功的页面如下图:
可以看到图 中地址栏里的地址多出了一个 ticket 参数,这就是 CAS 分配给当前应用的 ST(Service Ticket)。
此时,在浏览器中输入:
http://www.test.com:8080/SSO_Pro2/servlet/helloWorldExample ,则不用再次登录,到此,简单的单点登录已经实现。
5.定义自己的CAS验证机制
到这里,估计很多朋友肯定想问如何自定义自己的验证机制,比如使用咱们最常用的数据库:用户名/密码,验证的方式,
呵呵,别着急,下面就来和大家说一说如何来自定义CAS Server端的验证机制,咱们来一起实现一个基于mysql数据库 用户名/密码 验证的列子。
首先先和大家介绍一下CAS的扩展认证接口
CAS Server 负责完成对用户的认证工作,它会处理登录时的用户凭证 (Credentials) 信息,用户名/密码对是最常见的凭证信息。CAS Server 可能需要到数据库检索一条用户帐号信息,也可能在 XML 文件中检索用户名/密码,还可能通过 LDAP Server 获取等,在这种情况下,CAS 提供了一种灵活但统一的接口和实现分离的方式,实际使用中 CAS 采用哪种方式认证是与 CAS 的基本协议分离开的,用户可以根据认证的接口去定制和扩展
扩展 AuthenticationHandler
CAS 提供扩展认证的核心是 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() 方法前后执行某些其他操作,那么可以让自己的认证类扩展下面的抽象类:
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 提供了针对该方式的实现,如下所示:
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。
了解一下上面一段原 理后,咱们来写这个实际的例子吧:
cas-server-3.1.1-release.zip 包解开后,在 modules 目录下可以找到包 cas-server-support-jdbc-3.1.1.jar,其提供了通过 JDBC 连接数据库进行验证的缺省实现,基于该包的支持,我们只需要做一些配置工作即可实现 JDBC 认证。
[1].给出mysql建表语句,很简单,就一张user表,其他数据库也一样。
SET FOREIGN_KEY_CHECKS=0; -- ---------------------------- -- Table structure for user -- ---------------------------- CREATE TABLE `user` ( `id` int(11) NOT NULL auto_increment, `username` varchar(20) NOT NULL, `password` varchar(50) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- ---------------------------- -- Records -- ---------------------------- INSERT INTO `user` VALUES ('1', 'arix04', 'e10adc3949ba59abbe56e057f20f883e');
这里插了一条记录,用户名/密码:arix04/123456;使用的md5加密。
[2].配置
DataSource<bean id="casDataSource" class="org.apache.commons.dbcp.BasicDataSource"> <property name="driverClassName"> <value>com.mysql.jdbc.Driver</value> </property> <property name="url"> <value>jdbc:mysql://localhost:3306/cas_test</value> </property> <property name="username"> <value>root</value> </property> <property name="password"> <value>root</value> </property> </bean>
[3].配置 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 class="org.jasig.cas.authentication.handler.support.SimpleTestUsernamePassword
AuthenticationHandler" />,我们可以将其注释掉,换成我们希望的一个 AuthenticationHandler,比如,使用QueryDatabaseAuthenticationHandler
deployerConfigContext.xml文件插入下面这段:
<bean class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler"> <property name="dataSource" ref="casDataSource" /> <property name="sql" value="select password from user where username = ?" /> <property name="passwordEncoder" ref="myPasswordEncoder"/> </bean>
上面这段,sql定义了一个查询语句,用来判断用户名,密码是否存在,myPasswordEncoder是我自定义的一个密码的加密类,实现了passwordEncoder接口及其 encode() 方法。
[4].配置passwordEncoder
deployerConfigContext.xml文件插入下面这段:
<bean id="myPasswordEncoder" class="org.jasig.cas.authentication.handler.MyPasswordEncoder"/>
[5].MyPasswordEncoder
给出源码,大家自己编译成class吧,然后把MyPasswordEncoder.class放到
Tomcat 6.0\webapps\cas\WEB-INF\lib\cas-server-core-3.3.1.jar中相应的包下,jar包用winrar打开后,直接把class拖到相应目录下即可。
package org.jasig.cas.authentication.handler; import java.io.UnsupportedEncodingException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.SimpleDateFormat; import java.util.Date; import org.springframework.util.StringUtils; // Referenced classes of package org.jasig.cas.authentication.handler: // PasswordEncoder public final class MyPasswordEncoder implements PasswordEncoder { public MyPasswordEncoder(){}; public String encode(String password) { char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; try { byte[] strTemp = password.getBytes(); MessageDigest mdTemp = MessageDigest.getInstance("MD5"); mdTemp.update(strTemp); byte[] md = mdTemp.digest(); int j = md.length; char str[] = new char[j * 2]; int k = 0; for (int i = 0; i < j; i++) { byte byte0 = md[i]; str[k++] = hexDigits[byte0 >>> 4 & 0xf]; str[k++] = hexDigits[byte0 & 0xf]; } return new String(str); } catch (Exception e) { return null; } } public final static String MD5(String s) { char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; try { byte[] strTemp = s.getBytes(); MessageDigest mdTemp = MessageDigest.getInstance("MD5"); mdTemp.update(strTemp); byte[] md = mdTemp.digest(); int j = md.length; char str[] = new char[j * 2]; int k = 0; for (int i = 0; i < j; i++) { byte byte0 = md[i]; str[k++] = hexDigits[byte0 >>> 4 & 0xf]; str[k++] = hexDigits[byte0 & 0xf]; } return new String(str); } catch (Exception e) { return null; } } public static Date getDateByString(String dateString) { try { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); return dateFormat.parse(dateString); } catch (Exception e) { return null; } } public static String getDateString(Date date) { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); return dateFormat.format(date); } }
[6].放入必要的Jar
将开发准 备中的need.rar解压后的Jar文件、cas-server-support-jdbc-3.3.1.jar copy到Tomcat 6.0\webapps\cas\WEB-INF\lib\下面,如果你是用的其他数据库,则需要放入相应的数据库驱动包,
我这里给出的是mysql的驱动包。
[7].新建2个测试servlet
分别在上面SSO_Pro1,SSO_Pro2中新建class
WelcomePage
package servlet; import java.io.IOException; import java.io.PrintWriter; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import edu.yale.its.tp.cas.client.filter.CASFilter; import edu.yale.its.tp.cas.client.filter.CASFilterRequestWrapper; 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 SSO_Pro1 sample System!</title>"); out.println("</head>"); out.println("<body>"); out.println("<h1>Welcome to SSO_Pro1 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>"); } }
web.xml中添加
<servlet> <servlet-name>WelcomePage</servlet-name> <servlet-class>servlet.WelcomePage</servlet-class> </servlet> <servlet-mapping> <servlet-name>WelcomePage</servlet-name> <url-pattern>/servlet/welcomePage</url-pattern> </servlet-mapping>
[8]OK,测试一下
所有的配置结束后,下面我们启动tomcat来测试一下这个demo,浏览器中输入
http://www.test.com:8080/SSO_Pro1/WelcomePage
将跳转到CAS Server的登录界面,
输入用户名密码:arix04/123456,登录成功的话则跳转到
同样,浏览器中输入
http://www.test.com:8080/SSO_Pro2/WelcomePage
直接进入:
到这里,我们的例子已经基本完成了。这里省略了CAS Server的自定义界面配置,有兴趣的朋友可以自己研究一下了。
如果你的测试程序是在不同机器上面部署的话,那么你还需要注意一下:
与 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 所在机器建立信任关系。
三、备注
参数名 |
作用 |
edu.yale.its.tp.cas.client.filter.loginUrl |
指定 CAS 提供登录页面的 URL |
edu.yale.its.tp.cas.client.filter.validateUrl |
指定 CAS 提供 service ticket 或 proxy ticket 验证服务的 URL |
edu.yale.its.tp.cas.client.filter.serverName |
指定客户端的域名和端口,是指客户端应用所在机器而不是 CAS Server 所在机器,该参数或 serviceUrl 至少有一个必须指定 |
edu.yale.its.tp.cas.client.filter.serviceUrl |
该参数指定过后将覆盖 serverName 参数,成为登录成功过后重定向的目的地址 |
参数名 |
作用 |
edu.yale.its.tp.cas.client.filter.proxyCallbackUrl |
用于当前应用需要作为其他服务的代理(proxy) 时获取 Proxy Granting Ticket 的地址 |
edu.yale.its.tp.cas.client.filter.authorizedProxy |
用于允许当前应用从代理处获取 proxy tickets ,该参数接受以空格分隔开的多个 proxy URLs ,但实际使用只需要一个成功即可。当指定该参数过后,需要修改 validateUrl 到 proxyValidate ,而不再是 serviceValidate |
edu.yale.its.tp.cas.client.filter.renew |
如果指定为 true ,那么受保护的资源每次被访问时均要求用户重新进行验证,而不管之前是否已经通过 |
edu.yale.its.tp.cas.client.filter.wrapRequest |
如果指定为 true ,那么 CASFilter 将重新包装 HttpRequest, 并且使 getRemoteUser() 方法返回当前登录用户的用户名 |
edu.yale.its.tp.cas.client.filter.gateway |
指定 gateway 属性 |
传递登录用户名
CAS 在登录成功过后,会给浏览器回传 Cookie,设置新的到的 Service Ticket。但客户端应用拥有各自的 Session,我们要怎么在各个应用中获取当前登录用户的用户名呢?CAS Client 的 Filter 已经做好了处理,在登录成功后,就可以直接从 Session 的属性中获取,如清单 11 所示:
在 Java 中通过 Session 获取登录用户名
// 以下两者都可以
session.getAttribute(CASFilter.CAS_FILTER_USER); session.getAttribute("edu.yale.its.tp.cas.client.filter.user");
通过 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() 方法来获取登录用户名,具体方法如下所示:
通过 CASFilterRequestWrapper 获取登录用户名
CASFilterRequestWrapper reqWrapper=new CASFilterRequestWrapper(request); out.println("The logon user:" + reqWrapper.getRemoteUser());