單點登錄(SSO)是復雜應用系統的基本需求,Yale CAS是目前常用的開源解決方案。CAS認證中心,基於其特殊作用,自然會成為整個應用系統的核心,所有應用系統的認證工作,都將請求到CAS來完成。因此CAS服務器是整個應用的關鍵節點,CAS發生故障,所有系統都將陷入癱瘓。同時,CAS的負載能力要足夠強,能夠承擔所有的認證請求響應。利用負載均衡和集群技術,不僅能克服CAS單點故障,同時將認證請求分布到多台CAS服務器上,有效減輕單台CAS服務器的請求壓力。下面將基於CAS 3.4.5來討論下CAS集群。
CAS的工作原理,主要是基於票據(Ticket)來實現的(參見 CAS基本原理)。CAS票據,存儲在TicketRegistry中,因此要想實現CAS Cluster, 必須要多台CAS之間共享所有的Ticket,采用統一的TicketRegistry,可以達到此目的。 缺省的CAS實現中,TicketRegistry在內存中實現,不同的CAS服務器有自己單獨的TicketRegistry,因此是不支持分布式集群的。但CAS提供了支持TicketRegistry分布式的接口 org.jasig.cas.ticket.registry.AbstractDistributedTicketRegistry,我們可以實現這個接口實現多台CAS服務器TicketRegistry共享,從而實現CAS集群。
同時,較新版本CAS使用SpringWebFlow作為認證流程,而webflow需要使用session存儲流程相關信息,因此實現CAS集群,我們還得需要讓不同服務器的session進行共享。
我們采用內存數據庫Redis來實現TicketRegistry,讓多個CAS服務器共用同一個TicketRegistry。同樣方法,我們讓session也存儲在Redis中,達到共享session的目的。下面就說說如何用 Redis來實現TicketRegistry,我們使用Java調用接口Jedis來操作Redis,代碼如下:
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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
|
import
java.io.ByteArrayInputStream;
import
java.io.ByteArrayOutputStream;
import
java.io.ObjectInputStream;
import
java.io.ObjectOutputStream;
import
java.util.Collection;
import
org.jasig.cas.ticket.Ticket;
import
org.jasig.cas.ticket.TicketGrantingTicket;
import
org.jasig.cas.ticket.registry.AbstractDistributedTicketRegistry;
import
redis.clients.jedis.Jedis;
import
redis.clients.jedis.JedisPool;
import
redis.clients.jedis.JedisPoolConfig;
/*
* TicketRegistry using Redis, to solve CAS Cluster.
*
* @author ZL
*
*/
public
class
RedisTicketRegistry
extends
AbstractDistributedTicketRegistry {
private
static
int
redisDatabaseNum;
private
static
String hosts;
private
static
int
port;
private
static
int
st_time;
//ST最大空閑時間
private
static
int
tgt_time;
//TGT最大空閑時間
private
static
JedisPool cachePool;
static
{
redisDatabaseNum = PropertiesConfigUtil.getPropertyInt(
"redis_database_num"
);
hosts = PropertiesConfigUtil.getProperty(
"hosts"
);
port = PropertiesConfigUtil.getPropertyInt(
"port"
);
st_time = PropertiesConfigUtil.getPropertyInt(
"st_time"
);
tgt_time = PropertiesConfigUtil.getPropertyInt(
"tgt_time"
);
cachePool =
new
JedisPool(
new
JedisPoolConfig(), hosts, port);
}
public
void
addTicket(Ticket ticket) {
Jedis jedis = cachePool.getResource();
jedis.select(redisDatabaseNum);
int
seconds =
0
;
String key = ticket.getId() ;
if
(ticket
instanceof
TicketGrantingTicket){
//key = ((TicketGrantingTicket)ticket).getAuthentication().getPrincipal().getId();
seconds = tgt_time/
1000
;
}
else
{
seconds = st_time/
1000
;
}
ByteArrayOutputStream bos =
new
ByteArrayOutputStream();
ObjectOutputStream oos =
null
;
try
{
oos =
new
ObjectOutputStream(bos);
oos.writeObject(ticket);
}
catch
(Exception e){
log.error(
"adding ticket to redis error."
);
}
finally
{
try
{
if
(
null
!=oos) oos.close();
}
catch
(Exception e){
log.error(
"oos closing error when adding ticket to redis."
);
}
}
jedis.set(key.getBytes(), bos.toByteArray());
jedis.expire(key.getBytes(), seconds);
cachePool.returnResource(jedis);
}
public
Ticket getTicket(
final
String ticketId) {
return
getProxiedTicketInstance(getRawTicket(ticketId));
}
private
Ticket getRawTicket(
final
String ticketId) {
if
(
null
== ticketId)
return
null
;
Jedis jedis = cachePool.getResource();
jedis.select(redisDatabaseNum);
Ticket ticket =
null
;
ByteArrayInputStream bais =
new
ByteArrayInputStream(jedis.get(ticketId.getBytes()));
ObjectInputStream ois =
null
;
try
{
ois =
new
ObjectInputStream(bais);
ticket = (Ticket)ois.readObject();
}
catch
(Exception e){
log.error(
"getting ticket to redis error."
);
}
finally
{
try
{
if
(
null
!=ois) ois.close();
}
catch
(Exception e){
log.error(
"ois closing error when getting ticket to redis."
);
}
}
cachePool.returnResource(jedis);
return
ticket;
}
public
boolean
deleteTicket(
final
String ticketId) {
if
(ticketId ==
null
) {
return
false
;
}
Jedis jedis = cachePool.getResource();
jedis.select(redisDatabaseNum);
jedis.del(ticketId.getBytes());
cachePool.returnResource(jedis);
return
true
;
}
public
Collection<Ticket> getTickets() {
throw
new
UnsupportedOperationException(
"GetTickets not supported."
);
}
protected
boolean
needsCallback() {
return
false
;
}
protected
void
updateTicket(
final
Ticket ticket) {
addTicket(ticket);
}
}
|
同時,我們在ticketRegistry.xml配置文件中,將TicketRegistry實現類指定為上述實現。即修改下面的class值
1
2
3
4
5
|
<!-- Ticket Registry -->
<
bean
id
=
"ticketRegistry"
class
=
"org.jasig.cas.util.RedisTicketRegistry"
/>
<!-- <bean id="ticketRegistry" class="org.jasig.cas.ticket.registry.DefaultTicketRegistry" />
-->
|
因為使用了Redis的expire功能,注釋掉如下代碼:
1
2
3
4
5
6
7
8
9
10
11
12
|
<!-- TICKET REGISTRY CLEANER -->
lt;!-- <
bean
id
=
"ticketRegistryCleaner"
class
=
"org.jasig.cas.ticket.registry.support.DefaultTicketRegistryCleaner"
p:ticketRegistry-ref
=
"ticketRegistry"
/>
<
bean
id
=
"jobDetailTicketRegistryCleaner"
class
=
"org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean"
p:targetObject-ref
=
"ticketRegistryCleaner"
p:targetMethod
=
"clean"
/>
<
bean
id
=
"triggerJobDetailTicketRegistryCleaner"
class
=
"org.springframework.scheduling.quartz.SimpleTriggerBean"
p:jobDetail-ref
=
"jobDetailTicketRegistryCleaner"
p:startDelay
=
"20000"
p:repeatInterval
=
"5000000"
/> -->
|
通過上述實現TicketRegistry,多台CAS服務器就可以共用同一個 TicketRegistry。對於如何共享session,我們可以采用現成的第三方工具tomcat-redis-session-manager直接集成即可。對於前端web服務器(如nginx),做好負載均衡配置,將認證請求分布轉發給后面多台CAS,實現負載均衡和容錯目的。