CAS单点登录源码解析之【服务器端】

前期准备:
1.cas-server-3.5.2-release.zip
2.应用系统webapp1(http://127.0.0.1:8090/webapp1/main.do)
3.应用系统webapp2(http://127.0.0.1:8091/webapp2/main.do)
4.CAS单点登录服务器端(http://127.0.0.1:8081/cas-server/)
        本次讨论包括CAS单点登录服务器端的部分源码,以及在此基础上进行二次开发,因此需要修改部分CAS服务器端的源码,源码部分的修改在下面进行讨论。关于CAS客户端的源码分析,请参考另一篇文章http://blog.csdn.net/dovejing/article/details/44426547
其中cas-server-3.5.2-release.zip为CAS服务器端的源码zip包。

web.xml部分代码


    cas
    org.jasig.cas.web.init.SafeDispatcherServlet
   
        publishContext
        false
   

    1

    

    cas
    /login

    

    cas
    /logout

 

    cas
    /validate

 

    cas
    /serviceValidate

 

    cas
    /samlValidate

 

    cas
    /proxy

 

    cas
    /proxyValidate

 

    cas
    /CentralAuthenticationService

 

    cas
    /services/add.html

 

    cas
    /services/viewStatistics.html

 

    cas
    /services/logout.html

 

    cas
    /services/loggedOut.html

 

    cas
    /services/manage.html

 

    cas
    /services/edit.html

 

    cas
    /openid/*

 

    cas
    /services/deleteRegisteredService.html

    

    cas
    /services/updateRegisteredServiceEvaluationOrder.html

 

    cas
    /status

 

    cas
    /authorizationFailure.html

 

    cas
    /403.html

    

    cas
    /error

    

    cas
    /authcode

访问集成了CAS单点登录的应用系统webapp1
下面讲一下CAS单点登录服务器端的登录流程,流程的配置在/WEB-INF/login-webflow.xml文件中。

/WEB-INF/login-webflow.xml部分代码


首先,设置一个变量,用来存储用户名和密码信息。


    

整个登录流程从此处开始,流程初始化initialFlowSetupAction的配置信息在/WEB-INF/cas-servlet.xml中。

/WEB-INF/cas-servlet.xml部分代码

        p:argumentExtractors-ref="argumentExtractors"
        p:warnCookieGenerator-ref="warnCookieGenerator"
        p:ticketGrantingTicketCookieGenerator-ref="ticketGrantingTicketCookieGenerator"/>
其中argumentExtractors配置文件在/WEB-INF/spring-configuration/argumentExtractorsConfiguration.xml中。
/WEB-INF/spring-configuration/argumentExtractorsConfiguration.xml部分代码

    id="casArgumentExtractor"
    class="org.jasig.cas.web.support.CasArgumentExtractor"
    p:httpClient-ref="noRedirectHttpClient"
    p:disableSingleSignOut="${slo.callbacks.disabled:false}" />
 
    p:httpClient-ref="noRedirectHttpClient"
    p:disableSingleSignOut="${slo.callbacks.disabled:false}" />
     

    
    

其中warnCookieGenerator配置文件在/WEB-INF/spring-configuration/warnCookieGenerator.xml中。
/WEB-INF/spring-configuration/warnCookieGenerator.xml部分代码

    p:cookieSecure="true"
    p:cookieMaxAge="-1"
    p:cookieName="CASPRIVACY"
    p:cookiePath="/cas" />
其中ticketGrantingTicketCookieGenerator配置文件在/WEB-INF/spring-configuration/ticketGrantingTicketCookieGenerator.xml中。
/WEB-INF/spring-configuration/ticketGrantingTicketCookieGenerator.xml部分代码

    p:cookieSecure="false"
    p:cookieMaxAge="-1"
    p:cookieName="CASTGC"
    p:cookiePath="/cas" />
初始化部分会调用InitialFlowSetupAction的doExecute方法,如果有特殊需求,可以在此方法中增加相应的逻辑。如果希望单点登录集成统一身份认证,那么可以在此处增加统一身份认证的逻辑。关于CAS单点登录与统一身份认证的集成,我会单独写一篇。

InitialFlowSetupAction的doExecute方法

protected Event doExecute(final RequestContext context) throws Exception {
    final HttpServletRequest request = WebUtils.getHttpServletRequest(context);
    if (!this.pathPopulated) {
        final String contextPath = context.getExternalContext().getContextPath();
        final String cookiePath = StringUtils.hasText(contextPath) ? contextPath + "/" : "/";
        logger.info("Setting path for cookies to: " + cookiePath);
        this.warnCookieGenerator.setCookiePath(cookiePath);
        this.ticketGrantingTicketCookieGenerator.setCookiePath(cookiePath);
        this.pathPopulated = true;
    }
    //将TGT放在FlowScope作用域中
    context.getFlowScope().put(
        "ticketGrantingTicketId", this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request));
    //将warnCookieValue放在FlowScope作用域中
    context.getFlowScope().put(
        "warnCookieValue", Boolean.valueOf(this.warnCookieGenerator.retrieveCookieValue(request)));
    //获取service参数
    final Service service = WebUtils.getService(this.argumentExtractors, context);
 
    if (service != null && logger.isDebugEnabled()) {
        logger.debug("Placing service in FlowScope: " + service.getId());
    }
    //将service放在FlowScope作用域中
    context.getFlowScope().put("service", service);
 
    return result("success");
}
InitialFlowSetupAction的doExecute要做的就是把ticketGrantingTicketId,warnCookieValue和service放到FlowScope的作用域中,以便在登录流程中的state中进行判断。初始化完成后,登录流程流转到第一个state(ticketGrantingTicketExistsCheck)。

    

当我们第一次访问集成了CAS单点登录的应用系统webapp1时(http://127.0.0.1:8090/webapp1/main.do),此时应用系统会跳转到CAS单点登录的服务器端(http://127.0.0.1:8081/cas-server/login?service=http://127.0.0.1:8090/webapp1/main.do)。此时,request的cookies中不存在CASTGC(TGT),因此FlowScope作用域中的ticketGrantingTicketId为null,登录流程流转到第二个state(gatewayRequestCheck)。

             then="gatewayServicesManagementCheck" else="serviceAuthorizationCheck" />

因为初始化时,尽管把service保存在了FlowScope作用域中,但request中的参数gateway不存在,登录流程流转到第三个state(serviceAuthorizationCheck)。

    
    

ServiceAuthorizationCheck的doExecute方法

protected Event doExecute(final RequestContext context) throws Exception {
    final Service service = WebUtils.getService(context);
    //No service == plain /login request. Return success indicating transition to the login form
    if(service == null) {
        return success();
    }
    final RegisteredService registeredService = this.servicesManager.findServiceBy(service);
 
    if (registeredService == null) {
        logger.warn("Unauthorized Service Access for Service: [ {} ] - service is not defined in the service registry.", service.getId());
        throw new UnauthorizedServiceException();
    }
    else if (!registeredService.isEnabled()) {
        logger.warn("Unauthorized Service Access for Service: [ {} ] - service is not enabled in the service registry.", service.getId());
        throw new UnauthorizedServiceException();
    }
 
    return success();
}
ServiceAuthorizationCheck的doExecute方法,要做的就是判断FlowScope作用域中是否存在service,如果service存在,查找service的注册信息。登录流程流转到第四个state(generateLoginTicket)。

    
    

/WEB-INF/cas-servlet.xml部分代码
    p:ticketIdGenerator-ref="loginTicketUniqueIdGenerator" />
/WEB-INF/spring-configuration/uniqueIdGenerators.xml部分代码

             index="0"
        type="int"
        value="30" />

DefaultUniqueTicketIdGenerator要做的就是生成以LT作为前缀的loginTicket(例:LT-2-pfDmbEHfX2OkS0swLtDd7iDwmzlhsn)。注:LT只作为登录时使用的票据。

GenerateLoginTicketAction的generate方法

public final String generate(final RequestContext context) {
    //LT-2-pfDmbEHfX2OkS0swLtDd7iDwmzlhsn
    final String loginTicket = this.ticketIdGenerator.getNewTicketId(PREFIX);//生成loginTicket
    this.logger.debug("Generated login ticket " + loginTicket);
    WebUtils.putLoginTicket(context, loginTicket);//放到flowScope中
    return "generated";
}
GenerateLoginTicketAction的generate要做的就是生成loginTicket,并且把loginTicket放到FlowScope作用域中。登录流程流转到第五个state(viewLoginForm)。

    
        
        
    

    
        
    

        
    
        
    


        至此,经过五个state的流转,我们完成了第一次访问集成了单点登录的应用系统,此时流转到CAS单点登录服务器端的登录页面/WEB-INF/jsp/ui/default/casLoginView.jsp。由于casLoginView.jsp是CAS提供的默认登录页面,需要把此页面修改成我们系统需要的登录页面,格式需要参考casLoginView.jsp。

注意,默认的登录页面中有lt、execution和_eventId三个隐藏参数,lt参数值就是在GenerateLoginTicketAction的generate方法中生成的loginTicket。




下面说一下CAS单点登录服务器端的登录验证

当输入用户名和密码,点击登录按钮时,会执行AuthenticationViaFormAction的doBind方法。

    p:centralAuthenticationService-ref="centralAuthenticationService"
    p:warnCookieGenerator-ref="warnCookieGenerator" />
AuthenticationViaFormAction的doBind方法

public final void doBind(final RequestContext context, final Credentials credentials) throws Exception {
    final HttpServletRequest request = WebUtils.getHttpServletRequest(context);
    //bean中没有注入,这里什么也不做
    if (this.credentialsBinder != null && this.credentialsBinder.supports(credentials.getClass())) {
        this.credentialsBinder.bind(request, credentials);
    }
}
登录流程流转到第一个state(realSubmit),会执行AuthenticationViaFormAction的submit方法。

    
    
    
    
    
    
    
    
    
    

AuthenticationViaFormAction的submit方法

public final String submit(final RequestContext context, final Credentials credentials, final MessageContext messageContext) 
    throws Exception {
    // Validate login ticket
    final String authoritativeLoginTicket = WebUtils.getLoginTicketFromFlowScope(context);
    final String providedLoginTicket = WebUtils.getLoginTicketFromRequest(context);
    //判断FlowScope和request中的loginTicket是否相同
    if (!authoritativeLoginTicket.equals(providedLoginTicket)) {
        this.logger.warn("Invalid login ticket " + providedLoginTicket);
        final String code = "INVALID_TICKET";
        messageContext.addMessage(new MessageBuilder().error().code(code).arg(providedLoginTicket).defaultText(code).build());
        return "error";
    }
    //requestScope和FlowScope中获取TGT
    final String ticketGrantingTicketId = WebUtils.getTicketGrantingTicketId(context);
    //FlowScope中获取service
    final Service service = WebUtils.getService(context);
    if (StringUtils.hasText(context.getRequestParameters().get("renew")) 
            && ticketGrantingTicketId != null && service != null) {
 
        try {
            final String serviceTicketId = this.centralAuthenticationService.grantServiceTicket(
                ticketGrantingTicketId, service, credentials);
            WebUtils.putServiceTicketInRequestScope(context, serviceTicketId);
            putWarnCookieIfRequestParameterPresent(context);
            return "warn";
        } catch (final TicketException e) {
            if (isCauseAuthenticationException(e)) {
                populateErrorsInstance(e, messageContext);
                return getAuthenticationExceptionEventId(e);
            }
                
            this.centralAuthenticationService.destroyTicketGrantingTicket(ticketGrantingTicketId);
            if (logger.isDebugEnabled()) {
                logger.debug("Attempted to generate a ServiceTicket using renew=true with different credentials", e);
            }
        }
    }
 
    try {
        //根据用户凭证构造TGT,把TGT放到requestScope中,同时把TGT缓存到服务器的cache
        WebUtils.putTicketGrantingTicketInRequestScope(context, 
            this.centralAuthenticationService.createTicketGrantingTicket(credentials));
        putWarnCookieIfRequestParameterPresent(context);
        return "success";
    } catch (final TicketException e) {
        populateErrorsInstance(e, messageContext);
        if (isCauseAuthenticationException(e))
            return getAuthenticationExceptionEventId(e);
        return "error";
    }
}
AuthenticationViaFormAction的submit要做的就是判断FlowScope和request中的loginTicket是否相同。如果不同跳转到错误页面,如果相同,则根据用户凭证生成TGT(登录成功票据),并放到requestScope作用域中,同时把TGT缓存到服务器的cache中。登录流程流转到第二个state(sendTicketGrantingTicket)。
既然是登录,那么可以在此方法中加入自己的业务逻辑,比如,可以加入验证码的判断,以及错误信息的提示,用户名或者密码错误,验证码错误等逻辑判断。


    
    

SendTicketGrantingTicketAction的doExecute方法

protected Event doExecute(final RequestContext context) {
    //requestScope和FlowScope中获取TGT
    final String ticketGrantingTicketId = WebUtils.getTicketGrantingTicketId(context); 
    final String ticketGrantingTicketValueFromCookie = (String) context.getFlowScope().get("ticketGrantingTicketId");
        
    if (ticketGrantingTicketId == null) {
        return success();
    }
    //response中添加TGC
    this.ticketGrantingTicketCookieGenerator.addCookie(WebUtils.getHttpServletRequest(context), WebUtils
        .getHttpServletResponse(context), ticketGrantingTicketId);
 
    if (ticketGrantingTicketValueFromCookie != null && !ticketGrantingTicketId.equals(ticketGrantingTicketValueFromCookie)) {
        this.centralAuthenticationService
            .destroyTicketGrantingTicket(ticketGrantingTicketValueFromCookie);
    }
 
    return success();
}
SendTicketGrantingTicketAction的doExecute要做的是获取TGT,并根据TGT生成cookie添加到response。登录流程流转到第三个state(serviceCheck)。


    

由于此时FlowScope中存在service(http://127.0.0.1:8081/cas-server/login?service=http://127.0.0.1:8090/webapp1/main.do),登录流程流转到第四个state(generateServiceTicket)。


    
    
    
    

GenerateServiceTicketAction的doExecute方法

protected Event doExecute(final RequestContext context) {
    //获取service
    final Service service = WebUtils.getService(context);
    //获取TGT
    final String ticketGrantingTicket = WebUtils.getTicketGrantingTicketId(context);
 
    try {
        //根据TGT和service生成service ticket(ST-2-97kwhcdrBW97ynpBbZH5-cas01.example.org)
        final String serviceTicketId = this.centralAuthenticationService.grantServiceTicket(ticketGrantingTicket,
            service);
        //ST放到requestScope中
        WebUtils.putServiceTicketInRequestScope(context, serviceTicketId);
        return success();
    } catch (final TicketException e) {
        if (isGatewayPresent(context)) {
            return result("gateway");
        }
    }
 
    return error();
}
 
GenerateServiceTicketAction的doExecute要做的是获取service和TGT,并根据service和TGT生成以ST为前缀的serviceTicket(例:ST-2-97kwhcdrBW97ynpBbZH5-cas01.example.org),并把serviceTicket放到requestScope中。登录流程流转到第五个state(warn)。

    

由于此时FlowScope中不存在warnCookieValue,登录流程流转到第六个state(redirect)。

             result-type="org.jasig.cas.authentication.principal.Response" result="requestScope.response" />
    

从requestScope中获取serviceTicket,构造response对象,并把response放到requestScope中。登录流程流转到第七个state(postRedirectDecision)。

    

由于request请求(http://127.0.0.1:8081/cas-server/login?service=http://127.0.0.1:8090/webapp1/main.do)是get类型,登录流程流转到第八个state(redirectView)。

此时流程如下:

跳转到应用系统(http://127.0.0.1:8090/webapp1/main.do?ticket=ST-1-4hH2s5tzsMGCcToDvGCb-cas01.example.org)。
进入CAS客户端的AuthenticationFilter过滤器,由于session中获取名为“_const_cas_assertion_”的assertion对象不存在,但是request有ticket参数,所以进入到下一个过滤器。
TicketValidationFilter过滤器的validate方法通过httpClient访问CAS服务器端(http://127.0.0.1:8081/cas-server/serviceValidate?ticket=ST-1-4hH2s5tzsMGCcToDvGCb-cas01.example.org&service=http://127.0.0.1:8090/webapp1/main.do)验证ticket是否正确,并返回assertion对象。
Assertion对象格式类似于

    
        system
 
    


访问集成了CAS单点登录的应用系统webapp2
当我们第一次访问集成了CAS单点登录的应用系统webapp2时(http://127.0.0.1:8091/webapp2/main.do),此时应用系统会跳转到CAS单点登录的服务器端(http://127.0.0.1:8081/cas-server/login?service=http://127.0.0.1:8091/webapp2/main.do)。
InitialFlowSetupAction的doExecute初始化完成后,登录流程流转到第一个state(ticketGrantingTicketExistsCheck)。


    

因为应用系统webapp1已经成功登录,所以request的cookies中存在TGT,并保存到FlowScope中,登录流程流转到第二个state(hasServiceCheck)。

    

FlowScope中存在service,登录流程流转到第三个state(renewRequestCheck)。

             then="serviceAuthorizationCheck" else="generateServiceTicket" />

request中不存在renew,登录流程流转到第四个state(generateServiceTicket)。


    
    
    
    

后续的流转与应用系统webapp1相同,请参考前面webapp1的流转。

你可能感兴趣的:(JAVA)