CAS 单点登陆
一、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
- 到cas官网下载cas-server http://developer.jasig.org/cas/(我下载的是4.0.0)
- 解压压缩文件,在解压后的文件夹内找到/modules/cas-server-webapp-4.0.0.war。将其复制到%Tomcat_Home%\webapps下并改名为cas.war
- 启动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 自动跳转到认证页面。
2、登录成功后,自动转到 http://localhost:8080/cas-client-test1/index.jsp 页面。
3、访问 http://localhost:8080/cas-client-test2/index.jsp 因为已经被认证,所以可以直接打开不用再登录。
4、退出后打开退出页面,实际项目中可以通过 httpclient 请求登出地址来进行登出,然后跳转到登录页面。