CAS单点登录开源框架解读(十六)--单点登录服务端TGC校验,导致无法获取到tgt,响应401

单点登录情况下的现象

在单点登录情况,客户端应用接入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在未超时的情况下,客户端系统在单点登录下也还是要重新登录。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

行走的一只鞋

你的肯定是对我最大赞赏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值