您的位置:首页 > 运维架构 > Tomcat

Apache CAS部署在tomcat上实现单点登录

2016-04-08 23:33 549 查看
单点登录(Single Sign On , 简称 SSO )是目前比较流行的服务于企业业务整合的解决方案之一, SSO 使得在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。CAS(Central Authentication Service)是一款不错的针对 Web 应用的单点登录框架,本文介绍了 CAS 的原理、协议、在 Tomcat 中的配置和使用,对于采用 CAS 实现轻量级单点登录解决方案的入门读者具有一定指导作用。

1.CAS介绍

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 等。

2.CAS原理和协议

从结构上看,CAS 包含两个部分: CAS Server 和 CAS Client。CAS Server 需要独立部署,主要负责对用户的认证工作;CAS Client 负责处理对客户端受保护资源的访问请求,需要登录时,重定向到 CAS Server。图1 是 CAS 最基本的协议过程:



                                       图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 官方网站上的相关文档。

3.准备工作

下载CAS Server:https://www.apereo.org/projects/cas/download-cas

下载CAS Client :http://developer.jasig.org/cas-clients/

下载Tomcat : 本文用的是Tomcat 8.0,自己去官网下载即可。

4. 生成https的证书文件

为了在Tomcat启用SSL,使其支持https访问,需要生产证书文件。

这里需要使用JDK安装包下的keytool工具生成证书文件keystore,以命令方式换到目录%TOMCAT_HOME%,在command命令行输入如下命令: 

keytool -genkey -alias tomcat_key -keyalg RSA -storepass changeit -keystore server.keystore -validity 36500
此时会提示用户输入姓名,组织等信息,如下所示,只输入localhost作为name,其余回车即可



这样就会在当前路径下生成一个名为server.keystore的证书文件,这个文件会在后面tomcat的server.xml配置文件中用到。
简单解释下这个命令的含义:
-genkey 生成证书指令
 -alias 证书的别名为tomcat_key 
-keyalg 生成证书的算法是RSA 
-storepass 证书的密码changeit 
-keystore 生成的证书文件是server.keystore 这里也可以指定具体的路径,如在Mac下:~/Document表示用户目录下的Document文件夹下
-validity 36500 证书的有效期,单位是天,这里指定的是100年

5.配置 Tomcat 使用 Https 协议

在Tomcat的server.xml配置文件中进行如下配置:

<Connector SSLEnabled="true" clientAuth="false"
maxThreads="150" port="8443"
protocol="org.apache.coyote.http11.Http11NioProtocol"
scheme="https" secure="true" sslProtocol="TLS"
keystoreFile="/Users/cruise/Documents/ISG/apache-tomcat-8.0.32/server.keystore"
keystorePass="changeit" />


这里的keystoreFile需要指定上一步生成的证书文件,keystorePass对应生成文件的时候设置的密码。

6.部署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 已经部署成功,但这只是一个缺省的实现,在实际使用的时候,还需要根据实际概况做扩展和定制,最主要的是扩展认证 (Authentication) 接口和 CAS Server 的界面。

7. 扩展认证接口

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

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。

8.实现JDBC的认证

用户的认证信息通常保存在数据库中,因此本文就选用这种情况来介绍。将前面下载的 cas-server-{version}-release.zip 包解开后,在 modules 目录下可以找到包 cas-server-support-jdbc-{version}.jar,其提供了通过 JDBC 连接数据库进行验证的缺省实现,基于该包的支持,我们只需要做一些配置工作即可实现 JDBC 认证。

JDBC 认证方法支持多种数据库,DB2, Oracle, MySql, Microsoft SQL Server 等均可,这里以Mysql作为例子介绍。并且假设Mysql数据库名:test,数据库登录用户名: root,数据库登录密码:password,用户信息表为: t_user,该表包含用户名和密码的两个数据项分别为 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>root</value>
</property>
<property name="password">
<value>passw0rd</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 t_user where lower(username) = lower(?)" />
<!-- 指定密码加密器(可选) -->
<!-- <property name="passwordEncoder" ref="passwordEncoder" /> -->
</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所示:
清单 7. QueryDatabaseAuthenticationHandler配置加密器类

<!-- 密码加密器(可以指定自己实现的加密器) -->
<bean id="passwordEncoder"
class="org.jasig.cas.authentication.handler.DefaultPasswordEncoder" >
<constructor-arg name="encodingAlgorithm" value="MD5"/>
<property name="characterEncoding" value="UTF-8"/>
</bean>
本文这里由于未对数据库中的用户数据进行加密,所以在清单5中将配置的加密属性注释掉。

注意: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使用数据库中的用户名和密码进行登录测试了。

注意: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使用数据库中的用户名和密码进行登录测试了。

9.部署CAS客户端应用

单点登录的目的是为了让多个相关联的应用使用相同的登录过程,本文在讲解过程中构造 2个简单的应用,分别以 CASClient1 和 CASClient2 来作为示例,它们均只有一个页面,显示欢迎信息和当前登录用户名。这两个应用使用同一套登录信息,并且只有登录过的用户才能访问,通过本文的配置,实现单点登录,即只需登录一次就可以访问这两个应用。应用的工程结构很简单,引入必要的jar包,然后新建一个index.jsp页面。



index.jsp代码清单

<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>CASClient1's index.jsp</title>
</head>
<body>
<%
/*获取登录用户名*/
String username = request.getRemoteUser();

%>
current user: <%=username %>
<p><a href="https://localhost:8443/cas/logout">logout</a></p>
</body>
</html>


10.配置CAS客户端Filter
在CAS的client端的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后面增加参数来实现。

11.单点登录测试同时运行CAS server、 CASClient1 和CASClient2。分别访问下面的URL:

CASClient1:
localhost:8080/CASClient1/index.jsp

CASClient2: localhost:8080/CASClient2/index.jsp

当第一次访问上面的URL时,会重定向到下面的页面:



这时输入数据库中的用户名和对应的密码登录,登录成功后会看到如下页面:



此时访问第二个URL发现index页面会直接打开,而不用再次登录,因为访问CASClient1的时候已经认证过了。这就实现了所谓的单点登录。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息