CAS的集群环境,包括CAS的客户应用是集群环境,以及CAS服务本身是集群环境这两种情况。在集群环境下使用CAS,要解决两个问题,一是单点退出(注销)时,CAS如何将退出请求正确转发到用户session所在的具体客户应用服务器,而不是转发到其他集群服务器上,二是解决CAS服务端集群环境下各种Ticket信息的共享。
CAS集群部署
由于CAS Server是一个Web应用,因此可部署在Tomcat等容器中。直接部署CAS集群并使用负载均衡配置后,由于每次访问的CAS Server不固定,会发生通行证丢失。
解决方法:配置TOMCAT集群及Session复制,解决CAS Server Session复制。详细配置方法见"Nginx+Tomcat+Memcached集群"。
CAS的Ticket信息共享
当用户登录后,Ticket存储在CAS Server中,由于这部分数据未保存在Session中,仅靠TOMCAT Session复制无法解决问题。默认配置下,CAS Server使用org.jasig.cas.ticket.registry.DefaultTicketRegistry把Ticket数据保存在 HashMap中,因此多台CAS Server无法共享数据。导致用户登录及退出均存在问题。因此需要将Ticket信息进行共享存储,使多台CAS Server使用相同的存储区域管理Ticket。
Ticket信息共享处理比较简单,就是将Ticket信息从原来的内存存储变为数据库存储。见"ticketRegistry.xml"文件。处理方法有两种:1是将Ticket信息放在Redis内存数据库中,2是将Ticket信息放在memcached中,推荐使用memcached,现在DMGeoSSO已经支持这两种方式了,配置文件示例见"ticketRegistry.xml.redis"和ticketRegistry.xml.memcache。默认配置文件的内容为"ticketRegistry.xml.default"
Redis方式源代码:RedisTicketRegistry.java
Memcached方式源代码:MemCacheTicketRegistry.java
这里需要注意的是:TGT和ST的超时时间最大只能设置为30天(即2592000秒),多1秒都不行。这是Memcached的特性。
CAS单点注销
根据CAS Server工作流程,当收到Logout请求后,CAS Server会删除自身存储的有关当前用户的所有Ticket票据,"问题二"的解决方法已经解决了多台CAS Server删除票据的问题。但随后从CAS Server会发起HTTP POST请求到应用服务器,该请求中具备"logoutRequest"标志,应用服务器的SingleSignOutFilter接收到该请求后在应用服务器端进行用户登出操作。该操作主要是将应用服务器端的CAS Client中保存的用户Session数据失效,达到客户端登出效果。即,对于CAS系统,必须Server端和Client均进行登出操作,用户才会真正登出。cas退出采用的是异步操作,客户端是否退出成功也不关心。
CAS Server的这个工作流程,在应用集群部署的情况下带来一系列问题。由于应用服务器集群化,且一般会使用Session复制,当CAS Server向应用服务器发起Logout请求时,仅针对一台服务器发起请求,导致应用服务器没有全部退出,使得用户使用登出操作时,有时可以退出,有时不能退出,用户体验很差。
解决方法:采用广播方式,将单台Tomcat收到的注销请求广播给集群环境下的所有节点,达到所有服务器都注销的效果。核心代码:DMGeoSSOClient中的CasSingleLogoutClusterFilter.java。
配置方式,将DMGeoSSOClient工程下的lib目录下的jar包(servlet-api-2.3.jar除外)以及dist下的cas-client-core-3.1.3.jar包复制到集群环境所有Tomcat的lib目录,并修改所有tomcat的web.xml,增加过滤器的配置:
<filter>
<filter-name>CAS SLO Cluster Filter</filter-name>
<filter-class>org.esco.cas.client.CasSingleLogoutClusterFilter</filter-class>
<init-param>
<param-name>clientHostName</param-name>
<param-value>127.0.0.1:8080</param-value>
</init-param>
<init-param>
<param-name>peersUrls</param-name>
<param-value>http://127.0.0.1:8080,http://127.0.0.1:8081</param-value>
</init-param>
</filter>
clientHostName是本Tomcat的IP和端口,peersUrls是集群中所有节点的访问地址(格式为:协议://IP:端口,多个以","分隔),注意,这些IP地址和端口需要确保集群中所有的节点都能访问到。
另外,在集群中的所有需要做单点登录的应用中,web.xml中增加:
<filter-mapping>
<filter-name>CAS SLO Cluster Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
注意:这个过滤器需要放在原单点注销的过滤器之前才有效。
Nginx+Tomcat+Memcached集群(简)
nginx配置:
nginx.conf:
upstream cluster {
server 127.0.0.1:8080;
server 127.0.0.1:8081;
}
proxy.conf:
server {
listen 8888;
server_name 127.0.0.1;
#access_log logs/access.log main;
proxy_connect_timeout 60s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
proxy_buffer_size 1024k;
proxy_buffers 4 1024k;
proxy_busy_buffers_size 1024k;
proxy_temp_file_write_size 1024k;
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504 http_404;
proxy_max_temp_file_size 1024m;
location ~ ^/DMGeoPortal/ {
proxy_pass http://cluster;
proxy_set_header Host $host:$server_port;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location ~ ^/DMGeoSSO/ {
proxy_pass http://cluster;
proxy_set_header Host $host:$server_port;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
Tomcat配置:
context.xml:
<Manager className="de.javakaffee.web.msm.MemcachedBackupSessionManager"
memcachedNodes="n1:172.16.254.69:11211,n2:172.16.254.70:11211"
sticky="false"
sessionBackupAsync="false"
sessionBackupTimeout="18000"
transcoderFactoryClass="de.javakaffee.web.msm.serializer.javolution.JavolutionTranscoderFactory"
copyCollectionsForSerialization="false"
/>
server.xml:
<Engine name="Catalina" defaultHost="localhost">
注意:当sticky为false时,不需要配置jvmRoute属性,当sticky为true时,一定要配置jvmRoute属性,且集群中所有tomcat的jvmRoute属性均不一样。sticky的属性默认为true。在CAS服务器端集群和客户端集群环境下,需要将sticky配置为false,这样可以避免很多莫名其妙的session丢失问题。
Sticky 模式:tomcat session 为 主session, memcached 为备 session。Request请求到来时, 从memcached加载备 session 到 tomcat (仅当tomcat jvmroute发生变化时,否则直接取tomcat session);Request请求结束时,将tomcat session更新至memcached,以达到主备同步之目的。下面是sticky模式时响应的流程图(图片来源网络):
Non-Sticky模式:tomcat session 为 中转session, memcached1 为主 session,memcached 2 为备session。Request请求到来时,从memcached 2加载备 session 到 tomcat,(当 容器 中还是没有session 则从memcached1加载主 session 到 tomcat, 这种情况是只有一个memcached节点,或者有memcached1 出错时),Request请求结束时,将tomcat session更新至 主memcached1和备memcached2,并且清除tomcat session 。以达到主备同步之目的,如下是non-sticky模式的响应流程图:(图片来源网络)。
requestUriIgnorePattern属性不要设置。否则在CAS服务器端集群和客户端集群环境下有很多问题(包括影响注销功能,不能完全注销),因为我们的单点登录是对所有的资源进行拦截的,不需要设置requestUriIgnorePattern(URL忽略)属性。
集群中所有Tomcat的lib下新增的包:
javolution-5.4.3.1.jar
memcached-2.6.jar
memcached-session-manager-1.5.1.jar
memcached-session-manager-tc6-1.5.1.jar
msm-javolution-serializer-1.5.1.jar
msm-kryo-serializer-1.5.1.jar
msm-serializer-benchmark-1.5.1.jar
msm-xstream-serializer-1.5.1.jar
Nginx+IIS+Memcached集群(简)
nginx配置:
nginx.conf:
upstream dotnetcluster {
server 127.0.0.1:80;
server 127.0.0.1:81;
}
proxy.conf:
server {
listen 8888;
server_name 127.0.0.1;
#access_log logs/access.log main;
proxy_connect_timeout 60s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
proxy_buffer_size 1024k;
proxy_buffers 4 1024k;
proxy_busy_buffers_size 1024k;
proxy_temp_file_write_size 1024k;
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504 http_404;
proxy_max_temp_file_size 1024m;
location ~ ^/DMGeoGlobeWeb/ {
proxy_pass http://dotnetcluster;
proxy_set_header Host $host:8888;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location ~ ^/DMGeoMIS/ {
proxy_pass http://dotnetcluster;
proxy_set_header Host $host:8888;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
这里与Java应用稍有不同,注意上述红色加粗部分端口号的配置,该端口号是你最终访问集群服务器的端口号。
IIS配置:
将下列dll文件放在Web应用程序的bin目录:
Enyim.Caching.dll
MemcachedSessionProvider.dll
修改Web应用程序的Web.config配置:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<sectionGroup name="sessionManagement">
<section name="memcached" type="Enyim.Caching.Configuration.MemcachedClientSection, Enyim.Caching" />
</sectionGroup>
</configSections>
<sessionManagement>
<memcached protocol="Binary">
<servers>
<!-- make sure you use the same ordering of nodes in every configuration you have -->
<add address="172.16.254.69" port="11211" />
<add address="172.16.254.70" port="11211" />
</servers>
<locator type="MemcachedSessionProvider.SessionNodeLocator, MemcachedSessionProvider" />
</memcached>
</sessionManagement>
<system.web>
<sessionState customProvider="Memcached" mode="Custom">
<providers>
<add name="Memcached" type="MemcachedSessionProvider.SessionProvider, MemcachedSessionProvider" />
</providers>
</sessionState>
<machineKey validationKey="3A2122BF7DA69398B43FF26BD658CE428EC417BA" decryptionKey="5C90C7D3BE69792117E02AE72DDDAFBA853F5FDB3E57BC5C" decryption="3DES" validation="SHA1"/>
</system.web>
</configuration>
上述红色加粗部分是新增的配置项。说明如下:
sessionManagement:用来配置Memcached连接地址。
sessionState:用将配置将Session存储在什么地方,这里配置的是自定义,即将Session存储在Memcached中。
machineKey:这是比较关键的配置。如果我们的Web应用程序是在同一个IIS服务器上,用不同的端口来区分不同的网站应用,那么不需要配置machineKey;如果我们的Web应用程序是在不同的IIS服务器上,那么切记一定要配置machineKey。machineKey是对密钥进行配置,以便将其用于对 Forms 身份验证 Cookie 数据和视图状态数据进行加密和解密,并将其用于对进程外会话状态标识进行验证。默认情况下,Asp.Net的配置是自己动态生成,如果单台服务器当然没问题,但是如果多台服务器负载均衡,machineKey还采用动态生成的方式,每台服务器上的machinekey值不一致,就导致加密出来的结果也不一致,不能共享验证和 ViewState,所以对于多台服务器负载均衡的情况,一定要在每个站点配置相同的machineKey。
machineKey的生成算法如下:
private static string CreateKey(int len)
{
byte[] bytes = new byte[len];
new RNGCryptoServiceProvider().GetBytes(bytes);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < bytes.Length; i++)
{
sb.Append(string.Format("{0:X2}", bytes[i]));
}
return sb.ToString();
}
调用:
string validationKey = CreateKey(20);
string decryptionKey = CreateKey(24);
string machineKey = string.Format("<machineKey validationKey=\"{0}\" decryptionKey=\"{1}\" decryption=\"3DES\" validation=\"SHA1\"/>",validationKey, decryptionKey);