CAS 单点登陆

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/catoop/article/details/50530364

一、Tomcat配置SSL

1. 生成 server key

以命令方式换到目录%TOMCAT_HOME%,在command命令行输入如下命令:
keytool -genkey -alias tomcat_key -keyalg RSA -storepass changeit -keystore server.keystore -validity 3600
用户名输入域名,如localhost(开发或测试用)或 hostname.domainname(用户拥有的域名),其它全部以Enter跳过,最后确认,此时会在%TOMCAT_HOME%下生成server.keystore文件。

2. 将证书导入的JDK的证书信任库中

这步对于Tomcat的SSL配置不是必须,但对于CAS SSO是必须的,否则会出现如下错误:
edu.yale.its.tp.cas.client.CASAuthenticationException: Unable to validate ProxyTicketValidate.
导入过程分2步,第一步是导出证书,第二步是导入到证书信任库,命令如下:
keytool -export -trustcacerts -alias tomcat_key -file server.cer -keystore server.keystore -storepass changeit
keytool -import -trustcacerts -alias tomcat_key -file server.cer -keystore D:/”Program Files”/Java/jdk1.8.0_60/jre/lib/security/cacerts -storepass changeit
如果有提示,输入Y就可以了。
其它有用keytool命令(列出信任证书库中所有已有证书,删除库中某个证书):
keytool -list -keystore %JAVA_HOME%/jre/lib/security/cacerts >t.txt
keytool -delete -trustcacerts -alias tomcat_key -keystore %JAVA_HOME%/jre/lib/security/cacerts -storepass changeit
注意:CAS 建议不要使用 IP 地址,而要使用机器名或域名。

3.配置Tomcat

在Tomcat的server.xml配置文件中加入:

二、部署CAS Server到Tomcat

  1. 到cas官网下载cas-server http://developer.jasig.org/cas/(我下载的是4.0.0)
  2. 解压压缩文件,在解压后的文件夹内找到/modules/cas-server-webapp-4.0.0.war。将其复制到%Tomcat_Home%\webapps下并改名为cas.war
  3. 启动Tomcat,并测试 https://localhost:8443/cas 看是否访问正常(默认输入用户名和密码一致就可以)。
    注:CAS Server 4.0.0 默认登陆验证方式是 AcceptUsersAuthenticationHandler (老版本好像是SimpleTestUsernamePasswordAuthenticationHandler),默认用户名/密码为 casuser/Mellon(cas/WEB-INF/deployerConfigContext.xml 中找到 id=primaryAuthenticationHandler 的bean查看,里面的map也可以自己增加更多个)。我们通常需要从数据库中取出用户名和密码进行验证,所以我们需要修改 deployerConfigContext.xml,配置我们自己的服务认证方式。

三、配置服务认证

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

扩展 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 表示验证失败。

CAS中还提供了对 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() 成为了子类必须要实现的方法。

由于实际运用中,最常用的是用户名和密码方式的认证,CAS 提供了针对该方式的实现,如清单 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-{version}-release.zip 包解开后,在 modules 目录下可以找到包 cas-server-support-jdbc-{version}.jar,其提供了通过 JDBC 连接数据库进行验证的缺省实现,基于该包的支持,我们只需要做一些配置工作即可实现 JDBC 认证。

JDBC 认证方法支持多种数据库,DB2, Oracle, MySql, Microsoft SQL Server 等均可,这里以 DB2 作为例子介绍。并且假设DB2数据库名: CASTest,数据库登录用户名: db2user,数据库登录密码: db2password,用户信息表为: userTable,该表包含用户名和密码的两个数据项分别为 userName 和 password。

1. 配置 DataSource

打开文件 cas/WEB-INF/deployerConfigContext.xml,添加一个新的 bean 标签,以MYSQL为例,内容如清单 4 所示:

<!-- Data source definition --> 
<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/test</value> 
    </property> 
    <property name="username"> 
        <value>sites</value> 
    </property> 
    <property name="password"> 
        <value>123456</value> 
    </property>
</bean>

其中 casDataSource 在后面配置 AuthenticationHandler 会被引用,添加数据库驱动程序、连接地址、数据库登录用户名以及登录密码。

2. 配置 AuthenticationHandler

CAS 4.0.0 提供了 3 个基于 JDBC 的 AuthenticationHandler,分别为 BindModeSearchDatabaseAuthenticationHandler, QueryDatabaseAuthenticationHandler, SearchModeSearchDatabaseAuthenticationHandler。
- BindModeSearchDatabaseAuthenticationHandler 是用所给的用户名和密码去建立数据库连接,根据连接建立是否成功来判断验证成功与否;
- QueryDatabaseAuthenticationHandler 通过配置一个 SQL 语句查出密码,与所给密码匹配;
- SearchModeSearchDatabaseAuthenticationHandler 通过配置存放用户验证信息的表、用户名字段和密码字段,构造查询语句来验证。

使用哪个 AuthenticationHandler,需要在 deployerConfigContext.xml 中设置,默认情况下,CAS 4.0.0 使用 AcceptUsersAuthenticationHandler(上面已经提到,需要在 deployerConfigContext.xml 中查看)。

在 deployerConfigContext.xml 文件中可以找到如下配置:

<bean id="primaryAuthenticationHandler"
    class="org.jasig.cas.authentication.AcceptUsersAuthenticationHandler">
    <property name="users">
        <map>
            <entry key="casuser" value="Mellon"/>
        </map>
    </property>
</bean>

我们可以将其注释掉,换成我们希望的一个 AuthenticationHandler,比如,使用QueryDatabaseAuthenticationHandler 或 SearchModeSearchDatabaseAuthenticationHandler 可以分别选取清单 5 或清单 6 的配置。
清单 5. 使用 QueryDatabaseAuthenticationHandler

<bean id="primaryAuthenticationHandler" 
    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="passwordEncoder" />
</bean>
<!-- 密码加密器(可以指定自己实现的加密器) -->
<bean id="passwordEncoder"
    class="org.jasig.cas.authentication.handler.DefaultPasswordEncoder" >
    <constructor-arg name="encodingAlgorithm" value="MD5"/>
    <property name="characterEncoding" value="UTF-8"/>
</bean>

清单 6. 使用 SearchModeSearchDatabaseAuthenticationHandler

<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所示:

<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 中设置的实际加密器类的引用:

<bean id="passwordEncoder" class="org.jasig.cas.authentication.handler.MyPasswordEncoder"/>

这里 MyPasswordEncoder 是根据实际情况自己定义的加密器,实现 PasswordEncoder 接口及其 encode() 方法。
注意:DataSource 依赖于 commons-collections-3.2.jar、commons-dbcp-1.2.1.jar、commons-pool-1.3.jar、数据库驱动包、cas对jdbc的支持包cas-server-support-jdbc-4.0.0.jar,需要找到这几个jar包放进 %TOMCAT_HOME%/webapps/cas/WEB-INF/lib 目录。

然后便可以启动Tomcat使用数据库中的用户名和密码进行登录测试了。

四、扩展 CAS Server 界面

最直接的方法,下载官方源码,直接修改 ^_^
我想大家很有必要具备“任何开源项目都可以自己动手,使用通过源码构建项目并运行”的能力,万一你需要在源码上加点东西呢。
CAS 4.0 默认的页面存放在 cas/WEB-INF/view/jsp/default 下面,如果是源码项目页面存放在 cas-server-webapp\src\main\webapp\WEB-INF\view\jsp\default\ui 目录中。
我们来看一下 cas\WEB-INF\classes\default_views.properties 文件,如果是源码项目,文件位置为:cas-server-webapp\src\main\resources\default_views.properties

### Login view (/login)
casLoginView.(class)=org.springframework.web.servlet.view.JstlView
casLoginView.url=/WEB-INF/view/jsp/default/ui/casLoginView.jsp

### Display login (warning) messages
casLoginMessageView.(class)=org.springframework.web.servlet.view.JstlView
casLoginMessageView.url=/WEB-INF/view/jsp/default/ui/casLoginMessageView.jsp

### Login confirmation view (logged in, warn=true)
casLoginConfirmView.(class)=org.springframework.web.servlet.view.JstlView
casLoginConfirmView.url=/WEB-INF/view/jsp/default/ui/casConfirmView.jsp

### Logged-in view (logged in, no service provided)
casLoginGenericSuccessView.(class)=org.springframework.web.servlet.view.JstlView
casLoginGenericSuccessView.url=/WEB-INF/view/jsp/default/ui/casGenericSuccess.jsp

### Logout view (/logout)
casLogoutView.(class)=org.springframework.web.servlet.view.JstlView
casLogoutView.url=/WEB-INF/view/jsp/default/ui/casLogoutView.jsp

### CAS error view
viewServiceErrorView.(class)=org.springframework.web.servlet.view.JstlView
viewServiceErrorView.url=/WEB-INF/view/jsp/default/ui/serviceErrorView.jsp

viewServiceSsoErrorView.(class)=org.springframework.web.servlet.view.JstlView
viewServiceSsoErrorView.url=/WEB-INF/view/jsp/default/ui/serviceErrorSsoView.jsp

### CAS statistics view
viewStatisticsView.(class)=org.springframework.web.servlet.view.JstlView
viewStatisticsView.url=/WEB-INF/view/jsp/monitoring/viewStatistics.jsp

### Expired Password Error message
casExpiredPassView.(class)=org.springframework.web.servlet.view.JstlView
casExpiredPassView.url=/WEB-INF/view/jsp/default/ui/casExpiredPassView.jsp

### Locked Account Error message
casAccountLockedView.(class)=org.springframework.web.servlet.view.JstlView
casAccountLockedView.url=/WEB-INF/view/jsp/default/ui/casAccountLockedView.jsp

### Disabled Account Error message
casAccountDisabledView.(class)=org.springframework.web.servlet.view.JstlView
casAccountDisabledView.url=/WEB-INF/view/jsp/default/ui/casAccountDisabledView.jsp

### Must Change Password Error message
casMustChangePassView.(class)=org.springframework.web.servlet.view.JstlView
casMustChangePassView.url=/WEB-INF/view/jsp/default/ui/casMustChangePassView.jsp

### Bad Hours Error message
casBadHoursView.(class)=org.springframework.web.servlet.view.JstlView
casBadHoursView.url=/WEB-INF/view/jsp/default/ui/casBadHoursView.jsp

### Bad Workstation Error message
casBadWorkstationView.(class)=org.springframework.web.servlet.view.JstlView
casBadWorkstationView.url=/WEB-INF/view/jsp/default/ui/casBadWorkstationView.jsp

我们根据文件清单对应的文件修改即可。
如果你想保留这些页面,拷贝一份default 目录,然后修改其中对应的文件后,在这个配置文件中将对应路径修改便可。

除此之外,CAS 4.0 还提供了一些协议登录的方式,也就是在不通过登录页面使用账号密码的方式登录,自动授权登录。
比如我们可以从其他平台直接免登陆进入原来需要SSO登录后才可以看到的系统,或者做个U盾,插入U盾的电脑,可以直接访问系统,无需登录。
官方文档:http://jasig.github.io/cas/4.0.x/protocol/CAS-Protocol.html#cas-protocol

五、部署客户端应用

单点登录的目的是为了让多个相关联的应用使用相同的登录过程,本文在讲解过程中构造 2个简单的应用,分别以 casTest1 和 casTest2 来作为示例,它们均只有一个页面,显示欢迎信息和当前登录用户名。这 2 个应用使用同一套登录信息,并且只有登录过的用户才能访问,通过本文的配置,实现单点登录,即只需登录一次就可以访问这两个应用。
1. 与 CAS Server 建立信任关系
- CAS Server 部署在服务器A
- 客户端应用部署在服务B\C
由于客户端应用与 CAS Server 的通信采用 SSL,因此需要A与B\C的jre之间建立信任关系。
CAS Server 我们在文章的前面已经配置好的 SSL,现在我们只需要在客户端B\C上添加服务器A的证书,操作方法:

网上找一个这个文件下载下来 InstallCert.java
然后在客户端服务器上
编译:javac InstallCert.java
运行:java InstallCert serverA:8443 其中8443为SSL端口,serverA为服务器名称或域名
并且在接下来出现的询问中输入 1。这样,就将 A 添加到了 B 的 trust store 中(其他客户端服务器一样操作)。

这里要说一下的是 serverA:8443 这里不要写 {IP地址}:8443,要以域名的方式,如本机 localhost:8443 ,或域名方式 www.baidu.com:443

2. 配置 CAS Filter
在Client工程WEB-INF/lib下添加 cas-client-core-3.3.3.jar 包。
修改web.xml如下:

    <!-- ======================== 单点登录/登出 ======================== -->

    <!-- 该过滤器用于实现单点登出功能,可选配置。 -->
    <!--    登出地址 https://casserver:8443/cas/logout -->
    <filter>
        <filter-name>CAS Single Sign Out Filter</filter-name>
        <filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>
    </filter>

    <!-- 该过滤器负责用户的认证工作,必须启用它 -->
    <filter>
        <filter-name>CAS Authentication Filter</filter-name>
        <filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>
        <init-param>
            <param-name>casServerLoginUrl</param-name>
            <param-value>https://localhost:8443/cas/login</param-value>
        </init-param>
        <init-param>
            <param-name>serverName</param-name>
            <param-value>http://localhost:8080</param-value>
        </init-param>
    </filter>

    <!-- 该过滤器负责对Ticket的校验工作,必须启用它 -->
    <filter>
        <filter-name>CAS Validation Filter</filter-name>
        <filter-class>org.jasig.cas.client.validation.Cas10TicketValidationFilter</filter-class>
        <init-param>
            <param-name>casServerUrlPrefix</param-name>
            <param-value>https://localhost:8443/cas</param-value>
        </init-param>
        <init-param>
            <param-name>serverName</param-name>
            <param-value>http://localhost:8080</param-value>
        </init-param>
        <init-param>
            <param-name>redirectAfterValidation</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>

    <!-- 该过滤器负责实现HttpServletRequest请求的包装, 比如允许开发者通过HttpServletRequest的getRemoteUser()方法获得SSO登录用户的登录名,可选配置。 -->
    <filter>
        <filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
        <filter-class>org.jasig.cas.client.util.HttpServletRequestWrapperFilter</filter-class>
    </filter>

    <!-- 该过滤器使得开发者可以通过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 Single Sign Out Filter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <filter-mapping>
        <filter-name>CAS Authentication Filter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <filter-mapping>
        <filter-name>CAS Validation Filter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <filter-mapping>
        <filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <filter-mapping>
        <filter-name>CAS Assertion Thread Local Filter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <listener>
        <listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>
    </listener>
    <!-- ======================== 单点登录/登出结束 ======================== -->

运行Client工程,首次访问任一页面就会跳转到https://localhost:8443/cas/login进行认证。
把你的退出链接设置为:https://localhost:8443/cas/logout 即可实现单点登出,如果需要在系统退出后返回指定页面,我们可以修改源码在logout后面增加参数来实现。

3. 登录用户名的获取

// 1.
request.getRemoteUser();
// 2.
AssertionHolder.getAssertion().getPrincipal().getName()

六、单点登录测试截图

我创建了2个测试项目,首页访问路径分别为:
http://localhost:8080/cas-client-test1/index.jsp
http://localhost:8080/cas-client-test2/index.jsp

1、访问 http://localhost:8080/cas-client-test1/index.jsp 自动跳转到认证页面。

CAS 单点登陆

2、登录成功后,自动转到 http://localhost:8080/cas-client-test1/index.jsp 页面。

CAS 单点登陆

3、访问 http://localhost:8080/cas-client-test2/index.jsp 因为已经被认证,所以可以直接打开不用再登录。

CAS 单点登陆

4、退出后打开退出页面,实际项目中可以通过 httpclient 请求登出地址来进行登出,然后跳转到登录页面。

CAS 单点登陆

上一篇:Spring Boot 静态资源处理


下一篇:Spring Boot Controller