建立使用 Cas 进行单点登录的应用
目录
1.1 加入 cas-client-core-xxx.jar 到 classpath
1.2 配置 Filter
1.2.1 AuthenticationFilter
1.2.2 TicketValidationFilter
1.2.3 HttpServletRequestWrapperFilter
1.2.4 AssertionThreadLocalFilter
1.2.5 基于 Spring 的 Filter 配置
1.3 添加证书到信任库
根据之前的描述我们知道, Cas 由两部分组成, Cas Server 和 Cas Client 。 Cas Server 是 Cas 自己的服务端,而 Cas Client 是 Cas 客户端,其需要与我们自己的应用进行集成。
1.1 加入 cas-client-core-xxx.jar 到 classpath
在我们下载的 Cas Client 压缩包的 modules 目录下可以找到一个名为 cas-client-core-xxx.jar 的 jar 文件,首先需要将该 jar 包加入我们应用的类路径下,笔者这里使用的是 cas-client-core-3.1.11.jar 。如果用户的应用是使用 Maven 构造的,则可以在应用的 pom.xml 文件中加入如下依赖。
< dependency >
< groupId > org.jasig.cas.client </ groupId >
< artifactId > cas -client-core </ artifactId >
< version > 3.1.11 </ version >
</ dependency >
1.2 配置 Filter
然后需要我们在应用的 web.xml 文件中配置四个 Filter ,这四个 Filter 必须按照固定的顺序来进行配置,而且它们必须配置在应用的其它 Filter 之前。它们的先后顺序要求如下:
l AuthenticationFilter
l TicketValidationFilter
l HttpServletRequestWrapperFilter
l AssertionThreadLocalFilter
这些 Filter 有的必须指定某些参数,有的可以指定某些参数,这些参数可以通过 context-param 来指定,也可以通过 init-param 来指定。 Cas Client 默认会先从 init-param 取,没取到则从 context-param 取,所以当 init-param 和 context-param 都指定了某个参数时, init-param 指定的将拥有更高的优先级。所以当多个 Filter 需要共用一个参数时,我们可以把它定义为 context-param 。
1.2.1 AuthenticationFilter
AuthenticationFilter 用来拦截所有的请求,用以判断用户是否需要通过 Cas Server 进行认证,如果需要则将跳转到 Cas Server 的登录页面。如果不需要进行登录认证,则请求会继续往下执行。
AuthenticationFilter 有两个用户必须指定的参数,一个是用来指定 Cas Server 登录地址的 casServerLoginUrl ,另一个是用来指定认证成功后需要跳转地址的 serverName 或 service 。 service 和 serverName 只需要指定一个就可以了。当两者都指定了,参数 service 将具有更高的优先级,即将以 service 指定的参数值为准。 service 和 serverName 的区别在于 service 指定的是一个确定的 URL ,认证成功后就会确切的跳转到 service 指定的 URL ;而 serverName 则是用来指定主机名,其格式为 {protocol}:{hostName}:{port} ,如: https://localhost:8443 ,当指定的是 serverName 时, AuthenticationFilter 将会把它附加上当前请求的 URI ,以及对应的查询参数来构造一个确定的 URL ,如指定 serverName 为“ http://localhost ”,而当前请求的 URI 为“ /app ”,查询参数为“ a=b&b=c ”,则对应认证成功后的跳转地址将为“ http://localhost/app?a=b&b=c ”。
除了上述必须指定的参数外, AuthenticationFilter 还可以指定如下可选参数:
l renew :当指定 renew 为 true 时,在请 Cas Server 时将带上参数“ renew=true ”,默认为 false 。
l gateway :指定 gateway 为 true 时,在请求 Cas Server 时将带上参数“ gateway=true ”,默认为 false 。
l artifactParameterName :指定 ticket 对应的请求参数名称,默认为 ticket 。
l serviceParameterName :指定 service 对应的请求参数名称,默认为 service 。
如下是一个配置 AuthenticationFilter 的示例, serverName 由于在接下来配置的 Filter 中还要用,所以利用 context-param 将其配置为一个公用的参数。“ elim ”对应我的电脑名。
< context-param >
< param-name > serverName </ param-name >
< param-value > http://elim:8080 </ param-value >
</ context-param >
< filter >
< filter-name > casAuthenticationFilter </ filter-name >
< filter-class > org.jasig.cas.client.authentication.AuthenticationFilter </ filter-class >
< init-param >
< param-name > casServerLoginUrl </ param-name >
< param-value > https://elim:8443/cas/login </ param-value >
</ init-param >
</ filter >
< filter-mapping >
< filter-name > casAuthenticationFilter </ filter-name >
< url-pattern > /* </ url-pattern >
</ filter-mapping >
1.2.2 TicketValidationFilter
在请求通过 AuthenticationFilter 的认证之后,如果请求中携带了参数 ticket 则将会由 TicketValidationFilter 来对携带的 ticket 进行校验。 TicketValidationFilter 只是对验证 ticket 的这一类 Filter 的统称,其并不对应 Cas Client 中的一个具体类型。 Cas Client 中有多种验证 ticket 的 Filter ,都继承自 AbstractTicketValidationFilter ,它们的验证逻辑都是一致的,都有 AbstractTicketValidationFilter 实现,所不同的是使用的 TicketValidator 不一样。笔者这里将以 Cas10TicketValidationFilter 为例,其它还有 Cas20ProxyReceivingTicketValidationFilter 和 Saml11TicketValidationFilter 。
< filter >
< filter-name > casTicketValidationFilter </ filter-name >
< filter-class > org.jasig.cas.client.validation.Cas10TicketValidationFilter </ filter-class >
< init-param >
< param-name > casServerUrlPrefix </ param-name >
< param-value > https://elim:8443/cas </ param-value >
</ init-param >
</ filter >
< filter-mapping >
< filter-name > casTicketValidationFilter </ filter-name >
< url-pattern > /* </ url-pattern >
</ filter-mapping >
必须指定的参数:
l casServerUrlPrefix :用来指定 Cas Server 对应 URL 地址的前缀,如上面示例的“ https://elim:8443/cas ”。
l serverName 或 service :语义跟前面介绍的一致。
可选参数:
l redirectAfterValidation :表示是否验证通过后重新跳转到该 URL ,但是不带参数 ticket ,默认为 true 。
l useSession :在验证 ticket 成功后会生成一个 Assertion 对象,如果 useSession 为 true ,则会将该对象存放到 Session 中。如果为 false ,则要求每次请求都需要携带 ticket 进行验证,显然 useSession 为 false 跟 redirectAfterValidation 为 true 是冲突的。默认为 true 。
l exceptionOnValidationFailure :表示 ticket 验证失败后是否需要抛出异常,默认为 true 。
l renew :当值为 true 时将发送“ renew=true ”到 Cas Server ,默认为 false 。
1.2.3 HttpServletRequestWrapperFilter
HttpServletRequestWrapperFilter 用于将每一个请求对应的 HttpServletRequest 封装为其内部定义的 CasHttpServletRequestWrapper ,该封装类将利用之前保存在 Session 或 request 中的 Assertion 对象重写 HttpServletRequest 的 getUserPrincipal() 、 getRemoteUser() 和 isUserInRole() 方法。这样在我们的应用中就可以非常方便的从 HttpServletRequest 中获取到用户的相关信息。以下是一个配置 HttpServletRequestWrapperFilter 的示例:
< filter >
< filter-name > casHttpServletRequestWrapperFilter </ filter-name >
< filter-class > org.jasig.cas.client.util.HttpServletRequestWrapperFilter </ filter-class >
</ filter >
< filter-mapping >
< filter-name > casHttpServletRequestWrapperFilter </ filter-name >
< url-pattern > /* </ url-pattern >
</ filter-mapping >
1.2.4 AssertionThreadLocalFilter
AssertionThreadLocalFilter 是为了方便用户在应用的其它地方获取 Assertion 对象,其会将当前的 Assertion 对象存放到当前的线程变量中,那么以后用户在程序的任何地方都可以从线程变量中获取当前 Assertion ,无需再从 Session 或 request 中进行解析。该线程变量是由 AssertionHolder 持有的,我们在获取当前的 Assertion 时也只需要通过 AssertionHolder 的 getAssertion() 方法获取即可,如:
Assertion assertion = AssertionHolder. getAssertion ();
像 AssertionThreadLocalFilter 这种设计理念是非常好的,实际应用中使用的也比较多, Spring Security 中也有用到这种理念。为了便于大家了解,特贴出 AssertionHolder 的源码如下:
public class AssertionHolder {
/**
* ThreadLocal to hold the Assertion for Threads to access.
*/
private static final ThreadLocal threadLocal = new ThreadLocal();
/**
* Retrieve the assertion from the ThreadLocal.
*/
public static Assertion getAssertion() {
return (Assertion) threadLocal .get();
}
/**
* Add the Assertion to the ThreadLocal.
*/
public static void setAssertion( final Assertion assertion) {
threadLocal .set(assertion);
}
/**
* Clear the ThreadLocal.
*/
public static void clear() {
threadLocal .set( null );
}
}
以下是配置 AssertionThreadLocalFilter 的示例:
< filter >
< filter-name > casAssertionThreadLocalFilter </ filter-name >
< filter-class > org.jasig.cas.client.util.AssertionThreadLocalFilter </ filter-class >
</ filter >
< filter-mapping >
< filter-name > casAssertionThreadLocalFilter </ filter-name >
< url-pattern > /* </ url-pattern >
</ filter-mapping >
1.2.5 基于Spring的Filter配置
使用 Cas 单点登录的应用需要我们在应用的 web.xml 文件中配置上述介绍的四个 Filter ,但如果用户的应用是使用 Spring 开发的,则我们可以只在 web.xml 文件中配置四个 Spring 的 DelegatingFilterProxy 用来代理需要配置的四个 Filter ,对应的 Filter 名称对应我们需要代理的 Spring ApplicationContext 中 bean 的名称,此时我们需要将对应的 Filter 配置为 Spring ApplicationContext 中的一个 bean 对象。所以此时对应的 web.xml 文件的定义应该是这样的:
< filter >
< filter-name > casAuthenticationFilter </ filter-name >
< filter-class > org.springframework.web.filter.DelegatingFilterProxy </ filter-class >
</ filter >
< filter-mapping >
< filter-name > casAuthenticationFilter </ filter-name >
< url-pattern > /* </ url-pattern >
</ filter-mapping >
< filter >
< filter-name > casTicketValidationFilter </ filter-name >
< filter-class > org.springframework.web.filter.DelegatingFilterProxy </ filter-class >
</ filter >
< filter-mapping >
< filter-name > casTicketValidationFilter </ filter-name >
< url-pattern > /* </ url-pattern >
</ filter-mapping >
< filter >
< filter-name > casHttpServletRequestWrapperFilter </ filter-name >
< filter-class > org.springframework.web.filter.DelegatingFilterProxy </ filter-class >
</ filter >
< filter-mapping >
< filter-name > casHttpServletRequestWrapperFilter </ filter-name >
< url-pattern > /* </ url-pattern >
</ filter-mapping >
< filter >
< filter-name > casAssertionThreadLocalFilter </ filter-name >
< filter-class > org.springframework.web.filter.DelegatingFilterProxy </ filter-class >
</ filter >
< filter-mapping >
< filter-name > casAssertionThreadLocalFilter </ filter-name >
< url-pattern > /* </ url-pattern >
</ filter-mapping >
而对应的 Filter 应该都以对应的名称定义为 Spring ApplicationContext 中的一个 bean 。
< bean name = "casAuthenticationFilter"
class = "org.jasig.cas.client.authentication.AuthenticationFilter"
p:casServerLoginUrl = "https://elim:8443/cas/login" p:renew = "false"
p:gateway = "false" p:serverName = "http://elim:8080" />
< bean name = "casTicketValidationFilter"
class = "org.jasig.cas.client.validation.Cas10TicketValidationFilter"
p:serverName = "http://elim:8080" p:redirectAfterValidation = "true" >
< property name = "ticketValidator" >
< bean class = "org.jasig.cas.client.validation.Cas10TicketValidator" >
<!-- 对应于 casServerUrlPrefix -->
< constructor-arg index = "0" value = "https://elim:8443/cas" />
</ bean >
</ property >
</ bean >
< bean id = "casHttpServletRequestWrapperFilter" class = "org.jasig.cas.client.util.HttpServletRequestWrapperFilter" />
< bean id = "casAssertionThreadLocalFilter" class = "org.jasig.cas.client.util.AssertionThreadLocalFilter" />
1.3 添加证书到信任库
在 ticket 验证成功后,还需要验证证书,这需要我们将之前建立的证书导出并添加到当前 JRE 的证书信任库中,否则将验证失败。 JRE 在寻找证书时将根据当前使用的 host 来寻找,且会用该 host 匹配之前创建证书时指定的用户名称,如果匹配则表示找到。这也就意味着我们在 创建证书时指定的用户名称需要是我们的 host 。我的机器名称为“ elim ”,我就把它作为我的 host ,那么对应的证书应该这样创建。
keytool -genkey -keyalg RSA -alias tomcat -dname "cn=elim" -storepass changeit
该语句是对我们之前介绍的 keytool -genkey -alias tomcat -keyalg RSA 的精写,它已经通过相应的参数指定了对应的参数值,而不需要再与用户交互了。如果还用之前的语句生成证书的话,那么对应的值应该这样填:
之后会在用户的对应目录下生成一个 .keystore 文件。之后需要将该文件导出为一个证书到 %JAVA_HOME%/jre/lib/security 目录下,对应指令为:
keytool -export -alias tomcat -file %JAVA_HOME%/jre/lib/security/tomcat.crt -storepass changeit
之后需要将导出的 tomcat.crt 证书添加到运行时使用的 JRE 的受信任证书库中,此时如果出现异常可将原本 %JAVA_HOME%/jre/lib/security 目录下的 cacerts 删除后继续执行以下指令。
keytool -import -alias tomcat -file %JAVA_HOME%/jre/lib/security/tomcat.crt -keystore %JAVA_HOME%/jre/lib/security/cacerts -storepass changeit
经过以上几步后就可以启用我们自己的 Cas Client 应用了,然后初次访问该应用时就会跳转到 Cas Server 进行登录认证。认证成功后将跳转到我们自己的 Client 应用进行 ticket 的验证,验证通过后就可以自由的访问我们的 Client 应用了。
(注:本文是基于 Cas Server3.5.2 和 Cas Client3.1.11 所写)
(注:原创文章,转载请注明出处。原文地址: http://haohaoxuexi.iteye.com/blog/2142631 )