单点登录情况下的现象
在单点登录情况,客户端应用接入CAS服务端后,特别是外网环境会经常出现401的问题,要么直接跳转到登录页面,要么就直接响应401。注意此时CAS服务端的TGT并没有超时。只能排查后端的日志信息,开启debug模式看到了如下的后端异常信息:
[org.jasig.cas.web.support.TGCCookieRetrievingCookieGenerator] - Invalid cookie. Required remote address does not match XXX.XXX.XXX.XX
java.lang.IllegalStateException: Invalid cookie. Required remote address does not match XXX.XXX.XXX.XX
问题应该很明确,就是产生TGT时候,所使用的客户端IP和再次请求进来时所使用的客户端IP地址不一致导致无法通过校验。客户端浏览器就算有TGC的cookie存在,因为校验报错后也无法再走后续流程,直接到登录页面或者401。
CAS获取cookie中的TGC实现类
有了上面的错误信息,我们可以知道CAS服务端中获取到客户端浏览器中的tgc都是通过TGCCookieRetrievingCookieGenerator来实现的。我们来一起解析一下这个包中所有类相关的功能。(注意本系列所使用的CAS服务端的版本代码)。
类所在的包名为cas-server-webapp-cookie,其中的package中有如下的Java类。
org.jasig.cas.web.support.CookieRetrievingCookieGenerator |
---|
org.jasig.cas.web.support.CookieValueManager |
org.jasig.cas.web.support.DefaultCasCookieValueManager |
org.jasig.cas.web.support.NoOpCookieValueManager |
org.jasig.cas.web.support.TGCCookieRetrievingCookieGenerator |
主要看TGCCookieRetrievingCookieGenerator和DefaultCasCookieValueManager这两个类中是如何处理的。
CAS服务端的代码中获取cookie时,一般使用的都是默认CookieRetrievingCookieGenerator这个类,然后注入不同的实现
/** CookieGenerator for the TicketGrantingTickets. */
@NotNull
private CookieRetrievingCookieGenerator ticketGrantingTicketCookieGenerator;
/** 注入具体的实现*/
@Autowired
public void setTicketGrantingTicketCookieGenerator(
@Qualifier("ticketGrantingTicketCookieGenerator")
final CookieRetrievingCookieGenerator ticketGrantingTicketCookieGenerator) {
this.ticketGrantingTicketCookieGenerator = ticketGrantingTicketCookieGenerator;
}
解析获取tgc实现类中如何处理客户端信息
知道TGC获取的具体的代码为TGCCookieRetrievingCookieGenerator这个类,进行一下分析
@Component("ticketGrantingTicketCookieGenerator")
public class TGCCookieRetrievingCookieGenerator extends CookieRetrievingCookieGenerator {
/**
* Instantiates a new TGC cookie retrieving cookie generator.
*
* @param casCookieValueManager the cas cookie value manager
* 注意构造函数中使用的cookie管理类,这是我们需要重点分析,也是主要用于存取cookie的关键类。
*/
@Autowired
public TGCCookieRetrievingCookieGenerator(@Qualifier("defaultCookieValueManager")
final CookieValueManager casCookieValueManager) {
super(casCookieValueManager);
}
//定义cookie名称
@Override
@Autowired
public void setCookieName(@Value("${tgc.name:TGC}")
final String cookieName) {
super.setCookieName(cookieName);
}
//定义cookie中的路径
@Override
@Autowired
public void setCookiePath(@Value("${tgc.path:}")
final String cookiePath) {
super.setCookiePath(cookiePath);
}
//定义cookie的生命周期
@Override
@Autowired
public void setCookieMaxAge(@Value("${tgc.maxAge:-1}")
final Integer cookieMaxAge) {
super.setCookieMaxAge(cookieMaxAge);
}
//定义cookie是否安全
@Override
@Autowired
public void setCookieSecure(@Value("${tgc.secure:true}")
final boolean cookieSecure) {
super.setCookieSecure(cookieSecure);
}
//定义记住cookie的最大时间
@Override
@Autowired
public void setRememberMeMaxAge(@Value("${tgc.remember.me.maxAge:1209600}")
final int max) {
super.setRememberMeMaxAge(max);
}
通过TGCCookieRetrievingCookieGenerator的分析我们知道重点还是在cookie的管理类上,那么继续分析一下管理类中是如何实现的。
@Autowired
public DefaultCasCookieValueManager(@Qualifier("defaultCookieCipherExecutor")
final CipherExecutor<String, String> cipherExecutor) {
this.cipherExecutor = cipherExecutor;
LOGGER.debug("Using cipher [{} to encrypt and decode the cookie",
this.cipherExecutor.getClass());
}
@Override
public String buildCookieValue(final String givenCookieValue, final HttpServletRequest request) {
final StringBuilder builder = new StringBuilder(givenCookieValue);
final String remoteAddr = request.getRemoteAddr();
if (StringUtils.isBlank(remoteAddr)) {
throw new IllegalStateException("Request does not specify a remote address");
}
//加入用户客户端的IP地址
builder.append(COOKIE_FIELD_SEPARATOR);
builder.append(remoteAddr);
final String userAgent = request.getHeader("user-agent");
if (StringUtils.isBlank(userAgent)) {
throw new IllegalStateException("Request does not specify a user-agent");
}
//加入用户发起请求时的header头中的user-agent信息
builder.append(COOKIE_FIELD_SEPARATOR);
builder.append(userAgent);
final String res = builder.toString();
LOGGER.debug("Encoding cookie value [{}]", res);
//对数据进行加密
return this.cipherExecutor.encode(res);
}
@Override
public String obtainCookieValue(final Cookie cookie, final HttpServletRequest request) {
//解密cookie值
final String cookieValue = this.cipherExecutor.decode(cookie.getValue());
LOGGER.debug("Decoded cookie value is [{}]", cookieValue);
if (StringUtils.isBlank(cookieValue)) {
LOGGER.debug("Retrieved decoded cookie value is blank. Failed to decode cookie [{}]", cookie.getName());
return null;
}
//根据分割符分割cookie值
final String[] cookieParts = cookieValue.split(String.valueOf(COOKIE_FIELD_SEPARATOR));
if (cookieParts.length != COOKIE_FIELDS_LENGTH) {
throw new IllegalStateException("Invalid cookie. Required fields are missing");
}
final String value = cookieParts[0];
final String remoteAddr = cookieParts[1];
final String userAgent = cookieParts[2];
if (StringUtils.isBlank(value) || StringUtils.isBlank(remoteAddr)
|| StringUtils.isBlank(userAgent)) {
throw new IllegalStateException("Invalid cookie. Required fields are empty");
}
//校验tgc中产生的客户端IP地址和再次请求的IP地址是否一致
if (!remoteAddr.equals(request.getRemoteAddr())) {
throw new IllegalStateException("Invalid cookie. Required remote address does not match "
+ request.getRemoteAddr());
}
//校验tgc中产生的用户请求user-agent和再次发起请求的user-agent的值是否一致
if (!userAgent.equals(request.getHeader("user-agent"))) {
throw new IllegalStateException("Invalid cookie. Required user-agent does not match "
+ request.getHeader("user-agent"));
}
return value;
}
构造函数中使用的是对于写到客户端中的tgc的加密方式,不在这里展开,有兴趣的可自己深入。
看一下buildCookieValue这个方法,我们对响应客户端的cookie中写入了一些什么属性。
builder.append(remoteAddr); | 用户客户端的IP地址 |
---|---|
builder.append(userAgent); | 用户请求header头中的user-agent属性值 |
写入这两个信息到tgc的cookie中有什么用呢?看一下obtainCookieValue方法我们就一目了然了。
比较客户端IP地址是否一致
if (!remoteAddr.equals(request.getRemoteAddr())) {
throw new IllegalStateException("Invalid cookie. Required remote address does not match "
+ request.getRemoteAddr());
}
比较客户端请求头中user-agent的值是否一致
if (!userAgent.equals(request.getHeader(“user-agent”))) {
throw new IllegalStateException("Invalid cookie. Required user-agent does not match "
+ request.getHeader(“user-agent”));
}
总结
通过解读代码我们可知,在获取TGC的时候是要进行安全性校验的:客户端IP地址校验和客户端请求头user-agent的值校验。
1、如果在IP地址不固定,甚至是会漂移的情况下,那么在单点登录多系统集成甚至是单系统接入session超时后,都可以发生让你重新登录系统的情况。(简单测试:有限和无线切换之后,保证IP不一样,等待客户端会话超时,但是CAS服务端TGT未超时情况下,这时你就要重新登录。)
2、如果你的请求header头中改变了user-agent的值,那么很不幸,就是你tgt在未超时的情况下,客户端系统在单点登录下也还是要重新登录。