CAS项目登录流程介绍(一)

来源:互联网 发布:java访问者模式 编辑:程序博客网 时间:2024/05/23 00:57

CAS项目登录流程介绍

cas是目前比较流行的SSL项目,他对于用户登录认证和用户授权访问资源都是实行了自己的一套协议。

cas的登录行为是用spring-webflow来控制。


首先从登陆的提交表单开始介绍,通过post方法访问“/cas/login”路径,改路径对应着webflow的入口,已配置在spring中。

<bean class="org.springframework.webflow.mvc.servlet.FlowHandlerAdapter"p:flowExecutor-ref="flowExecutor" p:flowUrlHandler-ref="flowUrlHandler" /><bean id="flowUrlHandler" class="org.jasig.cas.web.flow.CasDefaultFlowUrlHandler" /><webflow:flow-executor id="flowExecutor" flow-registry="flowRegistry"><webflow:flow-execution-attributes><webflow:always-redirect-on-pause value="false" /></webflow:flow-execution-attributes><webflow:flow-execution-repository max-executions="50"  /></webflow:flow-executor><webflow:flow-registry id="flowRegistry" flow-builder-services="builder"><webflow:flow-location path="/WEB-INF/login-webflow.xml" id="login" /></webflow:flow-registry>
注意flowRegistry的id为login和我们访问路径一致。login和每个webflow的对应关系由org.springframework.webflow.mvc.servlet.FlowHandlerAdapter控制,有其方法handle完成。

public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)throws Exception {FlowHandler flowHandler = (FlowHandler) handler;checkAndPrepare(request, response, false);String flowExecutionKey = flowUrlHandler.getFlowExecutionKey(request);if (flowExecutionKey != null) {try {ServletExternalContext context = createServletExternalContext(request, response);FlowExecutionResult result = flowExecutor.resumeExecution(flowExecutionKey, context);handleFlowExecutionResult(result, context, request, response, flowHandler);} catch (FlowException e) {handleFlowException(e, request, response, flowHandler);}} else {try {String flowId = getFlowId(flowHandler, request);MutableAttributeMap input = getInputMap(flowHandler, request);ServletExternalContext context = createServletExternalContext(request, response);FlowExecutionResult result = flowExecutor.launchExecution(flowId, input, context);handleFlowExecutionResult(result, context, request, response, flowHandler);} catch (FlowException e) {handleFlowException(e, request, response, flowHandler);}}return null;}

注意getFlowId方法这里是决定启用哪个flow的地方。此处代码被flowExecutionKey 分成两段,是flowExecutionKey 确定每次请求是新的流程还是已经存在的流程中。


下面进入结合cas配置的web-flow来介绍下登录流程是如何进行的


首先是initialFlowSetupAction,每次都回运行这个bean,它被设置为on-start

<on-start>        <evaluate expression="initialFlowSetupAction" />    </on-start>
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;        }        context.getFlowScope().put(            "ticketGrantingTicketId", this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request));        context.getFlowScope().put(            "warnCookieValue",            Boolean.valueOf(this.warnCookieGenerator.retrieveCookieValue(request)));        final Service service = WebUtils.getService(this.argumentExtractors,            context);        if (service != null && logger.isDebugEnabled()) {            logger.debug("Placing service in FlowScope: " + service.getId());        }        context.getFlowScope().put("service", service);        return result("success");    }
其实doExecute方法只是将一次request的参数和cookie取出,放入flowscope中,在同一个流程中这些数据都将是共享的。

在这个方法中需要的参数为ticketGrantingTicketId(TGT),warnCookieValue,service。

之后就进入下一个流程,下一步很简单webflow里使用表达式判定来决定之后的流程走向。

<decision-state id="ticketGrantingTicketExistsCheck"><if test="flowScope.ticketGrantingTicketId != null" then="hasServiceCheck" else="gatewayRequestCheck" /></decision-state>    <decision-state id="gatewayRequestCheck"><if test="requestParameters.gateway != '' and requestParameters.gateway != null and flowScope.service != null " then="gatewayServicesManagementCheck" else="generateLoginTicket" /></decision-state><decision-state id="hasServiceCheck"><if test="flowScope.service != null" then="renewRequestCheck" else="viewGenericLoginSuccess" /></decision-state><decision-state id="renewRequestCheck"><if test="requestParameters.renew != '' and requestParameters.renew != null" then="generateLoginTicket" else="generateServiceTicket" /></decision-state>
这里判断条件比较多,我们先注意下flowScope.ticketGrantingTicketId和flowScope.service这两个参数。这是登陆过程所用到的。

首先进入ticketGrantingTicketExistsCheck状态,判断是否有flowScope.ticketGrantingTicketId也就是TGT,这里就把登陆流程分成了主要的两支。

用图来表示下这两个流程的不同:


renew和gateway是其他流程使用到的两个参数,所以上图简化了分支。

由图可知,如果有tgt则会进入generateServiceTicket状态(对应已经进过登陆认证),如没有则进入generateLoginTicket状态进行用户名密码检验用户认证。


由于在generateLoginTicket之后还是会进入generateServiceTicket状态,所以 我们查看generateLoginTicket状态。

<action-state id="generateLoginTicket">        <evaluate expression="generateLoginTicketAction.generate(flowRequestContext)" /><transition on="generated" to="generateTimestamp" /></action-state>

对应着generateLoginTicketAction的generate方法

这是个spring bean

<bean id="generateLoginTicketAction" class="org.jasig.cas.web.flow.GenerateLoginTicketAction"p:ticketIdGenerator-ref="loginTicketUniqueIdGenerator" />
具体代码

public final String generate(final RequestContext context) {        final String loginTicket = this.ticketIdGenerator.getNewTicketId(PREFIX);        this.logger.debug("Generated login ticket " + loginTicket);        WebUtils.putLoginTicket(context, loginTicket);        return "generated";    }

这里主要任务是生成一个loginTicket在flowscope里。

之后跳入generateTimestamp,因为这是和登录cas并无直接关系故在此处省略。


我们直接查看下一个状态viewLoginForm,这是一个登陆页面,其实所有的登陆行为是在这里开始的,前面的一些状态时在登录页面出现前已经进行初始化了。

<view-state id="viewLoginForm" view="casLoginView" model="credentials">        <binder>            <binding property="username" />            <binding property="password" />            <binding property="rememberMe" />        </binder>        <on-entry>            <set name="viewScope.commandName" value="'credentials'" />        </on-entry><transition on="submit" bind="true" validate="true" to="realSubmit">            <evaluate expression="authenticationViaFormAction.doBind(flowRequestContext, flowScope.credentials)" />        </transition>        <transition on="oauth" to="redirectOAuth" /></view-state>
点击提交进入realSubmit

<action-state id="realSubmit">        <evaluate expression="authenticationViaFormAction.submit(flowRequestContext, flowScope.credentials, messageContext)" />        <!--      To enable LPPE on the 'warn' replace the below transition with:      <transition on="warn" to="passwordPolicyCheck" />      CAS will attempt to transition to the 'warn' when there's a 'renew' parameter      and there exists a ticketGrantingId and a service for the incoming request.    --><transition on="warn" to="warn" /><!--      To enable LPPE on the 'success' replace the below transition with:      <transition on="success" to="passwordPolicyCheck" />    --><transition on="success" to="sendTicketGrantingTicket" /><transition on="error" to="generateLoginTicket" /><transition on="accountDisabled" to="casAccountDisabledView" />    <transition on="mustChangePassword" to="casMustChangePassView" />    <transition on="accountLocked" to="casAccountLockedView" />    <transition on="badHours" to="casBadHoursView" />    <transition on="badWorkstation" to="casBadWorkstationView" />    <transition on="passwordExpired" to="casExpiredPassView" /></action-state>
登陆授权的代码为

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);        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";        }        final String ticketGrantingTicketId = WebUtils.getTicketGrantingTicketId(context);        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 {            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";        }    }
这里代码也分成两部分,先获取TGT。如果有TGT并且有renew参数 则向cas申请ST

 final String serviceTicketId = this.centralAuthenticationService.grantServiceTicket(ticketGrantingTicketId, service, credentials);

如果没有则对其用户名密码进行身份认证,之后再获得TGT

 WebUtils.putTicketGrantingTicketInRequestScope(context, this.centralAuthenticationService.createTicketGrantingTicket(credentials));
对于一般登陆,大多数会跳入succes状态即sendTicketGrantingTicket状态

<action-state id="sendTicketGrantingTicket">        <evaluate expression="SendTicketGrantingTicketAction" /><transition to="serviceCheck" /></action-state>
protected Event doExecute(final RequestContext context) {        final String ticketGrantingTicketId = WebUtils.getTicketGrantingTicketId(context);         final String ticketGrantingTicketValueFromCookie = (String) context.getFlowScope().get("ticketGrantingTicketId");                if (ticketGrantingTicketId == null) {            return success();        }                this.ticketGrantingTicketCookieGenerator.addCookie(WebUtils.getHttpServletRequest(context), WebUtils            .getHttpServletResponse(context), ticketGrantingTicketId);        if (ticketGrantingTicketValueFromCookie != null && !ticketGrantingTicketId.equals(ticketGrantingTicketValueFromCookie)) {            this.centralAuthenticationService                .destroyTicketGrantingTicket(ticketGrantingTicketValueFromCookie);        }        return success();    }
sendTicketGrantingTicket的任务主要是将TGT写入cookie中作为用户已认证的凭据

之后进入serviceCheck状态,这个状态时判断请求参数是否带有service参数,这个参数决定登陆后跳转地址

<decision-state id="serviceCheck"><if test="flowScope.service != null" then="generateServiceTicket" else="viewGenericLoginSuccess" /></decision-state>
之后是generateServiceTicket,在经过登陆验证后,cas为每一个service所指向的url签发一个授权票据,告诉即将重定向的url,该用户已经过cas认证授权。st参数值会带在url后面,重定向的应用服务端将获取到此st并向cas进行确认。我们先来看一下,cas是如何签发st的

protected Event doExecute(final RequestContext context) {        final Service service = WebUtils.getService(context);        final String ticketGrantingTicket = WebUtils.getTicketGrantingTicketId(context);        try {            final String serviceTicketId = this.centralAuthenticationService                .grantServiceTicket(ticketGrantingTicket,                    service);            WebUtils.putServiceTicketInRequestScope(context,                serviceTicketId);            return success();        } catch (final TicketException e) {            if (isGatewayPresent(context)) {                return result("gateway");            }        }        return error();    }

由代码看出serviceTicketId是有ticketGrantingTicket,service参数共同确认,在获取st后将其放入上下文句柄。

到这里cas的登陆流程基本结束,还有以下几步,主要是负责跳回service所指向的服务,这里就不一一详列了。

<decision-state id="warn"><if test="flowScope.warnCookieValue" then="showWarningView" else="redirect" /></decision-state><action-state id="redirect">        <evaluate expression="flowScope.service.getResponse(requestScope.serviceTicketId)" result-type="org.jasig.cas.authentication.principal.Response" result="requestScope.response" />        <transition to="postRedirectDecision" />    </action-state>....................................

总结:cas的用户登录的url形式一般为*******/cas/login?service=%URL%.在访问这个url是,cas会先进入定制好的webflow,对一些基本参数进行初始化和验证。当用户为经认证时(TGT),cas为进入登陆页面供用户输入用户名密码进行登录。当用户有TGT并且有效时,将会根据service参数生成一个st出来。而用户在获得TGT后,总会进入生成st的流程。在TGT和ST都具备的情况下,才会跳回初始页面。这里说明一下,TGT存在于cookie,而st存在于url中。用户在cas的登陆状态是有tgt决定的,而是否能够访问原始又st决定。这种两段的验证授权方式,很好的解决了一处登陆,处处可访的情景。用户再已登陆的情况下,如果想访问和目前不同的网站应用,只需向cas申请一个st即可,无需再输入用户名密码了。

原创粉丝点击