DistChen

单点登录(SSO) CAS 实践

CAS ( Central Authentication Service ) 是 Yale 大学发起的一个企业级的、开源的项目,旨在为 Web 应用系统提供一种可靠的单点登录解决方法(属于 Web SSO )。在这篇文章(http://www.coin163.com/java/cas/cas.html)里面有对CAS 的一些介绍,这里我就不进行介绍了,我直接说我的实践过程和改造过程。

这里说下我对cas 这个sso解决方案的看法:cas基于session。因为我们每打开一个浏览器,都会生成一个session,该session 的id值 以cookie 的形式保存在浏览器中,当浏览器关闭或者手动设置 session 失效 invalidate的时候,保留session id 值的cookie会消失。所以当访问某个web应用程序时,如果request 所携带的头消息中的sessionid没有在sso验证中心检测到,那么就会自动跳转到sso验证中心提供的登录页面去进行登录,然后再将请求返回;如果有检测到对应的sessionid,那么就可以直接访问程序。所以cas的模式就是一个验证中心,多个客户端,登录、注销等页面由sso统一提供。

通过网上学习和自己的摸索实践,下面的过程是我完整实现单点登录的过程:

配置服务器

下载 cas-server

我下的最新版本cas-server-4.0.0,下载地址http://www.ja-sig.org/downloads/cas/;下载下来的文件是一个压缩包,解压后将modules文件中的cas-server-webapp-4.0.0.war 复制到tomcat 的webapps 目录中

这里写图片描述

注:压缩包内的其他文件夹都是modules文件夹中对应jar包的源码。是一个maven的聚合项目,所以最好使用maven来构建。
将cas-server-webapp-4.0.0.war 包放入 webapps目录后。启动tomcat,cas-server 就能自动发布并使用了。默认的登录页面如下:

这里写图片描述

更改语言

在上图中可以看到,显示语言是英语,我们需要改为中文。定位到cas-server-webapp-4.0.0\WEB-INF\classes文件夹,删除原有的message.properties 文件,复制messages_zh_CN.properties 并重命名为message.properties,重启tomcat,显示语言就会变成中文,如下所示

这里写图片描述

或者可以给对应页面的body加上 lang=’zh_CN’ 实现,如下所示:

这里写图片描述

配置用户数据源

可以看到在登录页面中有输入用户名和密码的输入框,我都没设置用户名数据的来源,输什么呢,在哪里设置用户数据来源呢?打开cas-server-webapp-4.0.0\WEB-INF\ deployerConfigContext.xml 文件,找到如下节点:

1
2
3
4
5
6
7
8
<bean id="primaryAuthenticationHandler"
class="org.jasig.cas.authentication.AcceptUsersAuthenticationHandler">
<property name="users">
<map>
<entry key="casuser" value="Mellon"/>
</map>
</property>
</bean>

这里就是用户数据来源,也就是说在之前的登录页面中,输入用户名 casuser,密码Mellon,就能正常登录了。如果这么指定用户数据来源,那也就太不科学了。Cas支持从数据库、ldap等获取数据,我用测试表(sys_users)配置了数据来源,注释掉上图中的bean,然后再该文件中新增如下两个bean,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<bean id="casDataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName">
<value>oracle.jdbc.driver.OracleDriver</value>
</property>
<property name="url">
<value>jdbc:oracle:thin:@127.0.0.1:1521:orcl</value>
</property>
<property name="username">
<value>sjzx</value>
</property>
<property name="password">
<value>suplis</value>
</property>
</bean>
<bean id="primaryAuthenticationHandler"
class="org.jasig.cas.adaptors.jdbc.SearchModeSearchDatabaseAuthenticationHandler"
abstract="false"
lazy-init="default" autowire="default">
<property name="tableUsers">
<value>sys_users</value>
</property>
<property name="fieldUser">
<value>loginname</value>
</property>
<property name="fieldPassword">
<value>userpwd</value>
</property>
<property name="dataSource" ref="casDataSource" />
</bean>

其中,id 为casDataSource 的bean指定了数据源,id 为 primaryAuthenticationHandler 指定了验证处理器的验证规则,即从casDataSource 取表名为 sys_users 当作用户数据表,loginname字段数据当作用户名,userpwd字段当作密码来进行验证。上图中的验证处理器除了SearchModeSearchDatabaseAuthenticationHandler类,还有一些其他的验证类,以便支持各种不同的数据来源。我们也可以扩展AuthenticationHandler 类来实现自己的验证器,比如QueryDatabaseAuthenticationHandler 验证器的使用方式如下:

1
2
3
4
5
<bean id="primaryAuthenticationHandler"
class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler">
<property name="dataSource" ref="casDataSource" />
<property name="sql" ref="select userpwd from sys_users where lower(loginname)=lower(?)"/>
</bean>

上图的效果与使用SearchModeSearchDatabaseAuthenticationHandler类的效果一样。注:需要将modules 文件夹中的cas-server-support-jdbc-4.0.0.jar包拷贝到lib下:

这里写图片描述

指定加密方式

cas如何将我们通过输入框输入的明文密码按照我们的加密方式加密成密文呢?所幸cas提供了这样的接口(不然就不科学了),同样的打开cas-server-webapp-4.0.0\WEB-INF\deployerConfigContext.xml文件,新增如下一个bean:

1
<bean id="passwordEncoder" class="dist.sso.EncryptDAP" />

然后为在上一步新增的id为primaryAuthenticationHandler 的bean中添加如下一个passwordEncoder属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<bean id="primaryAuthenticationHandler"
class="org.jasig.cas.adaptors.jdbc.SearchModeSearchDatabaseAuthenticationHandler"
abstract="false"
lazy-init="default" autowire="default">
<property name="tableUsers">
<value>sys_users</value>
</property>
<property name="fieldUser">
<value>loginname</value>
</property>
<property name="fieldPassword">
<value>userpwd</value>
</property>
<property name="dataSource" ref="casDataSource" />
<property name="passwordEncoder" ref="passwordEncoder"/>
</bean>

注: dist.sso.EncryptDAP类是我自行按照我的加密方式编译的一个类,不是cas提供的,该类要实现cas提供的 PasswordEncoder 接口及其 encode() 方法。

为 tomcat 启用 https

到了这里,就能用自己库中的数据来完成用户的验证了。虽然直接在验证中心可以验证通过了,但是在服务器集成的时候却还是有问题的,因为客户端在集成4.0.0版本的cas-server时,配置sso地址时只能配置sso的https地址,不能配置http地址(以前的版本可以,具体是从哪个版本开始有了这个限制我还没查到)。就像第一张图中的那样,当我们以http协议访问sso时,页面的显著位置会如下警告提示:

这里写图片描述

所以我们得为tomcat启用https支持。具体做法请参考我的另一篇文章《Tomat HTTPS 单向认证》

客户端集成sso

上一步完成后,cas 的验证中心就已经准备就绪了,接下来需要客户端集成到sso中了。Cas提供的方式是filter,也就是在请求具体某个web资源时,服务器会先将request发送到filter进行过滤,过滤通过后才将请求发送到具体的某个资源。

下载cas-client(我下载的 cas-client-3.1.12

下载地址http://www.ja-sig.org/downloads/cas-clients。在下载来的压缩包的modules文件夹中找到cas-client-core-3.1.12.jar 包,拷贝到具体某个 web程序的lib目录中

这里写图片描述

编辑具体web程序的 web.xml,添加如下的Listener 和 Filter;其中监听器是对注销操作的监听,这样当某个用户注销后,web程序能够立刻感知到,而filter是对用户的请求进行过滤,这个过滤操作会携带 request中的sessionid 到sso中去进行会话有效性的验证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<!-- ======================== 单点登录开始 ======================== -->
<!-- 用于单点退出,该过滤器用于实现单点登出功能,可选配置 -->
<listener>
<listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>
</listener>
<!-- 该过滤器用于实现单点登出功能,可选配置。 -->
<filter>
<filter-name>CAS Single Sign Out Filter</filter-name>
<filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CAS Single Sign Out Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 该过滤器负责用户的认证工作,必须启用它 -->
<filter>
<filter-name>CASFilter</filter-name>
<filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>
<init-param>
<param-name>casServerLoginUrl</param-name>
<param-value>https://localhost:8443/distsso/login</param-value>
<!--这里的server是服务端的IP -->
</init-param>
<init-param>
<param-name>serverName</param-name>
<param-value>http://localhost:8080</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CASFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 该过滤器负责对Ticket的校验工作,必须启用它 -->
<filter>
<filter-name>CAS Validation Filter</filter-name>
<filter-class>
org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter</filter-class>
<init-param>
<param-name>casServerUrlPrefix</param-name>
<param-value>https://localhost:8443/distsso</param-value>
</init-param>
<init-param>
<param-name>serverName</param-name>
<param-value>http://localhost:8080</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CAS Validation Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 该过滤器负责实现HttpServletRequest请求的包裹, 比如允许开发者通过HttpServletRequest的getRemoteUser()方法获得SSO登录用户的登录名,可选配置。 -->
<filter>
<filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
<filter-class>org.jasig.cas.client.util.HttpServletRequestWrapperFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 该过滤器使得开发者可以通过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 Assertion Thread Local Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- ======================== 单点登录结束 ======================== -->

至此,所有的准备工作已经完成,用户在请求某个集成了 cas 的程序时,会自动跳转到cas提供的登录页面去进行用户认证,验证通过后,再返回到最开始请求的页面。比如当我请求http://locahost:8080/DistSSOClient1时,页面会跳转到https://localhost:8443/distsso/login?service=http%3A%2F%2Flocalhost%3A8080%2FDistSSOClient1%2F,也就是 sso的登录界面,而且多了一个参数,该参数就是登录成功后的回调页面,也就是在跳转到该页面之前请求的页面(这个页面只完成当前登录用户的用户名显示),如下所示:

这里写图片描述

登录成功后,如下所示:

这里写图片描述

这个时候如果再去访问同样集成了cas的第二个程序 DistSSOClient2 ,就不在需要验证了,可以直接访问。如果验证失败,如下所示:

这里写图片描述

说明:上述的登录页面是我自己重新编写的,不足之处,烦请指正。

坚持原创技术分享,您的支持将鼓励我继续创作!