近期公司决定使用CAS来做单点登录,选择较为稳定的4.*版本,然而在集群部署时发现了不少问题。今天在这里总结一下,
一、部署条件
二、集群部署问题
-
需解决集群环境下CAS服务集群session共享问题。
-
需解决集群环境下多个CAS服务共用一个ticket库。
-
需解决集群环境和springwebflow框架下CAS登录流程数据加密秘钥统一(或去除登录流程数据加密)
-
需解决集群环境和springwebflow框架下CAS登录票据加密秘钥统一(或去除票据数据加密)
三、操作部署
-
配置tomcat服务器session共享:
DEMO简单配置:
(1)nginx配置两个tomcat服务器,server1、server2
(2)分别向两个tomcat导入所用jar包:E:\*** \***tomcat7***\lib 目录下。
commons-pool-1.6.jar、jedis-2.0.0.jar、tomcat-redis-session-manager-1.2-tomcat-7-java-7.jar
(3)分别修改tomcat配置文件:E:\*** \***tomcat7***\conf 目录下context.xml。
database="0"
host="127.0.0.1"
maxInactiveInterval="60"
port="6379"/>
(4)为了方便测试可修改服务器欢迎页:E:\*** \***tomcat7***\webapps\ROOT\index.jsp。
可删除ROOT目录下文件、重新写一个简单的index.jsp、增加服务器标识、展示sessionId。
启动两台tomcat服务器和nginx。访问服务器欢迎页显示两台服务器sessionId一致即可。
2.
CAS登录流程数据加密秘钥统一:
(1) 简要说明:
流程数据默认采用对称加密方式(AES),无手动配置的情况下会默认生成随机encryptionKey和signKey,
那么集群条件下就会产生问题,当集群中某个CAS服务在生成加密数据后去另一台CAS服务去解密,由于签名秘钥
分别由不同服务器随机产生的解密过程中就会产生错误。
(2) 涉及代码说明:
CAS4.2.7版本使用spring web flow框架,在执行登录流程调用loginFlowCipherBean对流程数据进行加密解密 时 使
用构造方法中默认注入的cipherExecutor子类
webflowCipherExecutor
webflowCipherExecutor使用时调用了构造方法,在构造方法中又调用该类的父类BinaryCipherExecutor,此时若无手动配置,
构造参数的secretKeyEncryption和secretKeySigning则皆为空。在BinaryCipherExecutor的构造方法中又判断这两个构造参数,若参数
为空则会产生随机key值用来加密解密。
(3) 解决方法一:
在CAS的配置文件cas.properties中打开webflow.encryption.key、webflow.signing.key注释,并配置值,将KEY值固定。
那么如何获取生成固定KEY值呢,其实源码BinaryCipherExecutor类中已经告诉我们了。自己写个main方法,导入jose4j的jar包就可以随机
生成。
public
static
void
main(String[]
args
) {
final
OctetSequenceJsonWebKey
octetKey
= OctJwkGenerator.
generateJwk
(512);
final
Map
params
=
octetKey
.toParams(org.jose4j.jwk.JsonWebKey.OutputControlLevel.
INCLUDE_SYMMETRIC
);
String
signingKey
=
params
.get(
"k"
).toString();
System.out.println("webflow.signing.key:"+signingKey);
String
encryptionKey
= RandomStringUtils.
randomAlphabetic
(16);
System.out.println("webflow.encryption.key:"+encryptionKey);
}
key的长度在BinaryCipherExecutor已有声明
(4) 解决方法二:
去除登录流程数据加密,CAS的util包中有一个类NoOpCipherExecutor.java和BinaryCipherExecutor一样是CipherExecutor子类,省略了对
数据的加密解密过程。
在CAS的配置文件cas-servlet.xml中配置:
启动服务时找不到这个类,则在applicationContext.xml中配置
org.jasig.cas.util.NoOpByteCipherExecutor"/>
其实,自己也可以仿照NoOpCipherExecutor自定义一个CipherExecutor,打成jar包,写进配置文件。
3. CAS登录cookie票据TGC加密秘钥统一:
(1) 简要说明:
和CAS登录流程数据加密解密的问题一样。
(2) 涉及代码说明:
登录验证成功后会生成TGC票据存储到浏览器的cookie中。
生成cookie时调用sendTicketGrantingTicketAction,然后再调用CookieRetrievingCookieGenerator
的addCookie方法,由于ticketGrantingTicketCookieGenerator注入了CookieRetrievingCookieGenerator
子类TGCCookieRetrievingCookieGenerator,其构造方法中注入了默认的cookieValueManager
,即是DefaultCasCookieValueManager。所以在执行CookieRetrievingCookieGenerator的addCookie方法时
所调用的实际上是CookieValueManager的子类DefaultCasCookieValueManager的buildCookieValue方法。
DefaultCasCookieValueManager构造方法中注入了defaultCookieCipherExecutor,在配置文件deployerConfigContext.xml
中默认配置的
是
TGCCipherExecutor,
所以在
DefaultCasCookieValueManager的
buildCookieValue方法中实际调用了TGCCipherExecutor的encode方法,即是
TGCCipherExecutor的父类BaseStringCipherExecutor的endcode方法。在调用TGCCipherExecutor时,
若无手动配置,其
构造参数secretKeyEncryption,secretKeySigning皆为空,又调用了其父类,BaseStringCipherExecutor的构造方法中对
这两个构造参数进行了判断,若参数
为空则会产生随机key值用来加密解密。
(3) 解决方法一:
在CAS的配置文件cas.properties中打开tgc.encryption.key、tgc.signing.key注释,并配置值,将KEY值固定。
那么如何获取生成固定KEY值呢,其实源码BaseStringCipherExecutor类中也已经告诉我们了。自己写个main方法,导入
jose4j的jar包就可以随机
生成。
public static void main(String[] args) {
final OctetSequenceJsonWebKey octetKey = OctJwkGenerator.generateJwk(512);
final Map params = octetKey.toParams(org.jose4j.jwk.JsonWebKey.OutputControlLevel.INCLUDE_SYMMETRIC);
String signingKey = params.get("k").toString();
System.out.println("tgc.signing.key:"+signingKey);
final OctetSequenceJsonWebKey octetKey2 = OctJwkGenerator.generateJwk(256);
final Map params2 = octetKey2.toParams(org.jose4j.jwk.JsonWebKey.OutputControlLevel.INCLUDE_SYMMETRIC);
String signingKey2 = params2.get("k").toString();
System.out.println("tgc.encryption.key:"+signingKey2);
}
(4) 解决方法二:
去除票据数据加密,CAS中有一个类noOpCookieValueManager,省略了对票据的加密解密过程。可在deployerConfigContext.xml文件中配置
4. 使用redis作为集群公共缓存ticket的仓库:
(1) 简要说明:
单独CAS生成TGT票据时默认缓存在所在服务器内存中,所以集群环境下如果生成票据和认证票据的请求在不同的服务器上,那么会造成票据
认证错误。
(2) 解决方法:
① 可建立本地工程,新建 AbstractDistributedTicketRegistry 的继承类 :
import
java.util.Collection;
import
java.util.HashSet;
import
java.util.Set;
import
java.util.concurrent.TimeUnit;
import
javax.validation.constraints.Min;
import
javax.validation.constraints.NotNull;
import
org.jasig.cas.ticket.ServiceTicket;
import
org.jasig.cas.ticket.Ticket;
import
org.jasig.cas.ticket.TicketGrantingTicket;
import
org.jasig.cas.ticket.registry.AbstractDistributedTicketRegistry;
import
org.springframework.beans.factory.DisposableBean;
public
class
RedisTicketRegistry
extends
AbstractDistributedTicketRegistry
implements
DisposableBean {
/**
redis
client. */
@NotNull
private
final
TicketRedisTemplate
redisTemplate
;
/**
* TGT cache entry timeout in seconds.
*/
@Min
(0)
private
final
int
tgtTimeout
;
/**
* ST cache entry timeout in seconds.
*/
@Min
(0)
private
final
int
stTimeout
;
private
final
String
PREFIX_CAS
=
"CAS:TICKET:"
;
public
RedisTicketRegistry(TicketRedisTemplate
redisTemplate
,
int
tgtTimeout
,
int
stTimeout
) {
this
.
redisTemplate
=
redisTemplate
;
this
.
tgtTimeout
=
tgtTimeout
;
this
.
stTimeout
=
stTimeout
;
}
@Override
public
void
addTicket(Ticket
ticket
) {
logger
.info(
"Add ticket {}"
,
ticket
);
try
{
this.redisTemplate.boundValueOps(PREFIX_CAS+ticket.getId()).set(ticket, getTimeout(ticket), TimeUnit.SECONDS);
}
catch
(Exception
e
) {
e.printStackTrace();
}
}
@Override
public
Ticket getTicket(String
ticketId
) {
logger
.info(
"Get ticket {}"
,
ticketId
);
try
{
Ticket
t
= (Ticket)
this
.
redisTemplate
.boundValueOps(
PREFIX_CAS
+
ticketId
).get();
if
(
t
!=
null
) {
return
getProxiedTicketInstance(
t
);
}
}
catch
(
final
Exception
e
) {
logger
.error(
"Failed fetching {} "
,
ticketId
,
e
);
}
return
null
;
}
@Override
public
Collection getTickets() {
Set
tickets
=
new
HashSet();
Set
keys
=
this
.
redisTemplate
.keys(
PREFIX_CAS
+
"*"
);
for
(String
key
:
keys
) {
Ticket
ticket
= (Ticket)
this
.
redisTemplate
.boundValueOps(
key
).get();
if
(
ticket
==
null
)
this
.
redisTemplate
.delete(
key
);
else
{
tickets
.add(
ticket
);
}
}
return
tickets
;
}
@Override
protected
boolean
needsCallback() {
//
TODO
Auto-generated method stub
return
true
;
}
@Override
protected
void
updateTicket(Ticket
ticket
) {
logger.info("Updating ticket {}",ticket);
try
{
this.redisTemplate.boundValueOps(PREFIX_CAS+ticket.getId()).set(ticket, getTimeout(ticket), TimeUnit.SECONDS);
}
catch
(
final
Exception
e
) {
logger
.error(
"Failed updating {}"
,
ticket
,
e
);
}
}
@Override
public
boolean
deleteSingleTicket(String
ticketId
) {
logger
.debug(
"Deleting Single Ticket {}"
,
ticketId
);
try
{
this
.
redisTemplate
.delete(
PREFIX_CAS
+
ticketId
);
return
true
;
}
catch
(
final
Exception
e
) {
logger
.error(
"Failed deleting {}"
,
ticketId
,
e
);
}
return
false
;
}
@Override
public
void
destroy()
throws
Exception {
//
TODO
Auto-generated method stub
}
/**
* Gets the timeout value for the ticket.
*
*
@param
t the t
*
@return
the timeout
*/
private
int
getTimeout(
final
Ticket
t
) {
if
(
t
instanceof
TicketGrantingTicket) {
return
this
.
tgtTimeout
;
}
else
if
(
t
instanceof
ServiceTicket) {
return
this
.
stTimeout
;
}
throw
new
IllegalArgumentException(
"Invalid ticket type"
);
}
}
② 新建
RedisTemplate的继承类
import org.jasig.cas.ticket.Ticket;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
public
class
TicketRedisTemplate extends RedisTemplate{
public TicketRedisTemplate()
{
RedisSerializer string =
new
StringRedisSerializer();
JdkSerializationRedisSerializer jdk =
new
JdkSerializationRedisSerializer();
setKeySerializer(string);
setValueSerializer(jdk);
setHashKeySerializer(string);
setHashValueSerializer(jdk);
}
public
TicketRedisTemplate(RedisConnectionFactory connectionFactory)
{
setConnectionFactory(connectionFactory);
afterPropertiesSet();
}
}
③ 修改配置文件:
注释掉deployerConfigContext.xml中的票据仓库配置配置:
在applicationContext.xml中加入配置
class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"
p:hostName="127.0.0.1"
p:database="0"
p:usePool="true"
p:pool-config-ref="poolConfig"/>
p:connection-factory-ref="jedisConnFactory">
到此,配置完成,可以完成集群部署的SSO功能。