`
guangzhao
  • 浏览: 43108 次
  • 性别: Icon_minigender_1
  • 来自: 北京
最近访客 更多访客>>
文章分类
社区版块
存档分类
最新评论
阅读更多
定义自己的CAS验证机制
到这里,估计很多朋友肯定想问如何自定义自己的验证机制,比如使用咱们最常用的数据库:用户名/密码,验证的方式,
呵呵,别着急,下面就来和大家说一说如何来自定义CAS Server端的验证机制,咱们来一起实现一个基于mysql数据库 用户名/密码 验证的列子。

首先先和大家介绍一下CAS的扩展认证接口
CAS Server 负责完成对用户的认证工作,它会处理登录时的用户凭证 (Credentials) 信息,用户名/密码对是最常见的凭证信息。CAS Server 可能需要到数据库检索一条用户帐号信息,也可能在 XML 文件中检索用户名/密码,还可能通过 LDAP Server 获取等,在这种情况下,CAS 提供了一种灵活但统一的接口和实现分离的方式,实际使用中 CAS 采用哪种方式认证是与 CAS 的基本协议分离开的,用户可以根据认证的接口去定制和扩展

扩展 AuthenticationHandler
CAS 提供扩展认证的核心是 AuthenticationHandler 接口,该接口定义如下:
Code
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() 方法前后执行某些其他操作,那么可以让自己的认证类扩展下面的抽象类:
Code
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 提供了针对该方式的实现,如下所示:
Code
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表,其他数据库也一样。
Code
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
找到tomcat/webapps/cas/WEB-INF目录(这里建议在做一下操作时先删除cas.war,只留下已经解包的cas工程)
找到deployerConfigContext.xml文件插入下面这段:
Code
<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文件插入下面这段:
Code
                <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拖到相应目录下即可。
Code
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
Code
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中添加
Code
    <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 所在机器建立信任关系。

三、备注

表格 1. CASFilter 必需的参数

参数名


作用

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 参数,成为登录成功过后重定向的目的地址



表格 2. CASFilter 可选参数

参数名


作用

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());
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics