分享

jasig CAS登录验证分析

 时间要去哪 2014-08-05

jasig CAS登录验证分析:

之前文章讲到了怎么利用jasig CAS实现sso:

http://my.oschina.net/indestiny/blog/200768

本文对jasig CAS验证过程做个简单的分析,便于以后能够更好定制自己的CAS, 要了解CAS流程你需要知道spring,springmvc等知识,也要了解spring-webflow, 因为整个验证流程都是由spring-webflow定制的,你可以参考我转载的一篇spring-webflow的文章:

http://my.oschina.net/indestiny/blog/201988

ok, 就开始了。

  • 先说说我们未登录状态时:

重点就是服务器端的配置:WEB-INF/login-webflow.xml中,它定义了整个登录流程,我们先就分析其流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
<flow xmlns="http://www./schema/webflow"
      xmlns:xsi="http://www./2001/XMLSchema-instance"
      xsi:schemaLocation="http://www./schema/webflow
                          http://www./schema/webflow/spring-webflow-2.0.xsd">
    <var name="credentials" class="org.jasig.cas.authentication.principal.UsernamePasswordCredentials" />
    <on-start>
        <evaluate ="initialFlowSetupAction" />
    </on-start>
    <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="serviceAuthorizationCheck" />
    </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="serviceAuthorizationCheck" else="generateServiceTicket" />
    </decision-state>
    <!-- Do a service authorization check early without the need to login first -->
    <action-state id="serviceAuthorizationCheck">
        <evaluate ="serviceAuthorizationCheck"/>
        <transition to="generateLoginTicket"/>
    </action-state>
     
    <!--
        The "warn" action makes the determination of whether to redirect directly to the requested
        service or display the "confirmation" page to go back to the server.
    -->
    <decision-state id="warn">
        <if test="flowScope.warnCookieValue" then="showWarningView" else="redirect" />
    </decision-state>
     
    <!--
    <action-state id="startAuthenticate">
        <action bean="x509Check" />
        <transition on="success" to="sendTicketGrantingTicket" />
        <transition on="warn" to="warn" />
        <transition on="error" to="generateLoginTicket" />
    </action-state>
     -->
     
    <!--
        LPPE transitions begin here: You will also need to
        move over the 'lppe-configuration.xml' file from the
        'unused-spring-configuration' folder to the 'spring-configuration' folder
        so CAS can pick up the definition for the bean 'passwordPolicyAction'.
    -->
    <action-state id="passwordPolicyCheck">
        <evaluate ="passwordPolicyAction" />
        <transition on="showWarning" to="passwordServiceCheck" />
        <transition on="success" to="sendTicketGrantingTicket" />
        <transition on="error" to="viewLoginForm" />
    </action-state>
    <action-state id="passwordServiceCheck">
        <evaluate ="sendTicketGrantingTicketAction" />
        <transition to="passwordPostCheck" />
    </action-state>
    <decision-state id="passwordPostCheck">
        <if test="flowScope.service != null" then="warnPassRedirect" else="pwdWarningPostView" />
    </decision-state>
    <action-state id="warnPassRedirect">
        <evaluate ="generateServiceTicketAction" />
        <transition on="success" to="pwdWarningPostView" />
        <transition on="error" to="generateLoginTicket" />
        <transition on="gateway" to="gatewayServicesManagementCheck" />
    </action-state>
    <end-state id="pwdWarningAbstractView">
        <on-entry>
            <set name="flowScope.passwordPolicyUrl" value="passwordPolicyAction.getPasswordPolicyUrl()" />
        </on-entry>
    </end-state>
    <end-state id="pwdWarningPostView" view="casWarnPassView" parent="#pwdWarningAbstractView" />
    <end-state id="casExpiredPassView" view="casExpiredPassView" parent="#pwdWarningAbstractView" />
    <end-state id="casMustChangePassView" view="casMustChangePassView" parent="#pwdWarningAbstractView" />
    <end-state id="casAccountDisabledView" view="casAccountDisabledView" />
    <end-state id="casAccountLockedView" view="casAccountLockedView" />
    <end-state id="casBadHoursView" view="casBadHoursView" />
    <end-state id="casBadWorkstationView" view="casBadWorkstationView" />
    <!-- LPPE transitions end here... -->
     
    <action-state id="generateLoginTicket">
        <evaluate ="generateLoginTicketAction.generate(flowRequestContext)" />
        <transition on="generated" to="viewLoginForm" />
    </action-state>
     
    <view-state id="viewLoginForm" view="casLoginView" model="credentials">
        <binder>
            <binding property="username" />
            <binding property="password" />
        </binder>
        <on-entry>
            <set name="viewScope.commandName" value="'credentials'" />
        </on-entry>
        <transition on="submit" bind="true" validate="true" to="realSubmit">
            <evaluate ="authenticationViaFormAction.doBind(flowRequestContext, flowScope.credentials)" />
        </transition>
    </view-state>
    <action-state id="realSubmit">
        <evaluate ="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>
     
    <action-state id="sendTicketGrantingTicket">
        <evaluate ="sendTicketGrantingTicketAction" />
        <transition to="serviceCheck" />
    </action-state>
    <decision-state id="serviceCheck">
        <if test="flowScope.service != null" then="generateServiceTicket" else="viewGenericLoginSuccess" />
    </decision-state>
     
    <action-state id="generateServiceTicket">
        <evaluate ="generateServiceTicketAction" />
        <transition on="success" to ="warn" />
        <transition on="error" to="generateLoginTicket" />
        <transition on="gateway" to="gatewayServicesManagementCheck" />
    </action-state>
    <action-state id="gatewayServicesManagementCheck">
        <evaluate ="gatewayServicesManagementCheck" />
        <transition on="success" to="redirect" />
    </action-state>
    <action-state id="redirect">
        <evaluate ="flowScope.service.getResponse(requestScope.serviceTicketId)" result-type="org.jasig.cas.authentication.principal.Response" result="requestScope.response" />
        <transition to="postRedirectDecision" />
    </action-state>
    <decision-state id="postRedirectDecision">
        <if test="requestScope.response.responseType.name() == 'POST'" then="postView" else="redirectView" />
    </decision-state>
    <!--
        the "viewGenericLogin" is the end state for when a user attempts to login without coming directly from a service.
        They have only initialized their single-sign on session.
    -->
    <end-state id="viewGenericLoginSuccess" view="casLoginGenericSuccessView" />
    <!--
        The "showWarningView" end state is the end state for when the user has requested privacy settings (to be "warned") to be turned on.  It delegates to a
        view defines in default_views.properties that display the "Please click here to go to the service." message.
    -->
    <end-state id="showWarningView" view="casLoginConfirmView" />
    <end-state id="postView" view="postResponseView">
        <on-entry>
            <set name="requestScope.parameters" value="requestScope.response.attributes" />
            <set name="requestScope.originalUrl" value="flowScope.service.id" />
        </on-entry>
    </end-state>
    <!--
        The "redirect" end state allows CAS to properly end the workflow while still redirecting
        the user back to the service required.
    -->
    <end-state id="redirectView" view="externalRedirect:${requestScope.response.url}" />
     
    <end-state id="viewServiceErrorView" view="viewServiceErrorView" />
     
    <end-state id="viewServiceSsoErrorView" view="viewServiceSsoErrorView" />
    <global-transitions>
        <!-- CAS-1023 This one is simple - redirects to a login page (same as renew) when 'ssoEnabled' flag is unchecked
             instead of showing an intermediate unauthorized view with a link to login page -->
        <transition to="viewLoginForm" on-exception="org.jasig.cas.services.UnauthorizedSsoServiceException"/>
        <transition to="viewServiceErrorView" on-exception="org.springframework.webflow.execution.repository.NoSuchFlowExecutionException" />
        <transition to="viewServiceErrorView" on-exception="org.jasig.cas.services.UnauthorizedServiceException" />
    </global-transitions>
</flow>
首先设置了一个变量 credentials来保存用户名及密码信息:

1
<var name="credentials" class="org.jasig.cas.authentication.principal.UsernamePasswordCredentials" />
在该flow执行一开始,做一次初始化:

1
2
3
<on-start>
     <evaluate ="initialFlowSetupAction" />
</on-start>

对应其配置在/WEB-INF/cas-servlet.xml中:

1
2
3
4
<bean id="initialFlowSetupAction" class="org.jasig.cas.web.flow.InitialFlowSetupAction"
        p:argumentExtractors-ref="argumentExtractors"
        p:warnCookieGenerator-ref="warnCookieGenerator"
        p:ticketGrantingTicketCookieGenerator-ref="ticketGrantingTicketCookieGenerator"/>

其中argumentExtractors配置/WEB-INF/spring-configuration/argumentExtractorsConfiguration.xml中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<bean
    id="casArgumentExtractor"
    class="org.jasig.cas.web.support.CasArgumentExtractor"
    p:httpClient-ref="noRedirectHttpClient"
    p:disableSingleSignOut="${slo.callbacks.disabled:false}" />
<bean id="samlArgumentExtractor" class="org.jasig.cas.web.support.SamlArgumentExtractor"
    p:httpClient-ref="noRedirectHttpClient"
    p:disableSingleSignOut="${slo.callbacks.disabled:false}" />
     
 <util:list id="argumentExtractors">
    <ref bean="casArgumentExtractor" />
    <ref bean="samlArgumentExtractor" />
 </util:list>

其中ticketGrantingTicketCookieGenerator配置在/WEB-INF/spring-configuration/ticketGrantingTicketCookieGenerator.xml:

1
2
3
4
5
<bean id="ticketGrantingTicketCookieGenerator" class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator"
    p:cookieSecure="true"
    p:cookieMaxAge="-1"
    p:cookieName="CASTGC"
    p:cookiePath="/cas" />

其中warnCookieGenerator的配置在/WEB-INF/spring-configuration/warnCookieGenerator.xml:

1
2
3
4
5
<bean id="warnCookieGenerator" class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator"
    p:cookieSecure="true"
    p:cookieMaxAge="-1"
    p:cookieName="CASPRIVACY"
    p:cookiePath="/cas" />
对应会调用InitialFlowSetupAction的doExecute方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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);
     context.getFlowScope().put("service", service);
     return result("success");
}

讲完初始化flow配置,看看第一个state(ticketGrantingTicketExistsCheck), 当第一次登录cas时(https://cas_server:8443/cas/login), 没有ticketGrantingTicketId, 所以会留向gatewayRequestCheck state:

1
2
3
<decision-state id="ticketGrantingTicketExistsCheck">
    <if test="flowScope.ticketGrantingTicketId != null" then="hasServiceCheck" else="gatewayRequestCheck" />
</decision-state>
看gatewayRequestCheck state,第一次service也是为null, 所以流向serviceAuthorizationCheck state:
1
2
3
<decision-state id="gatewayRequestCheck">
    <if test="requestParameters.gateway != '' and requestParameters.gateway != null and flowScope.service != null" then="gatewayServicesManagementCheck" else="serviceAuthorizationCheck" />
</decision-state>
继续看serviceAuthorizationCheck state, 其会先调用 org.jasig.cas.web.flow.ServiceAuthorizationCheck的doExecute方法,之后流向generateLoginTicket,生成ticket:
1
2
3
4
<action-state id="serviceAuthorizationCheck">
    <evaluate ="serviceAuthorizationCheck"/>
    <transition to="generateLoginTicket"/>
</action-state>
看generateLoginTicket state, 调用generateLoginTicketAction.generate方法来生成ticket,返回给客户端:
1
2
3
4
<action-state id="generateLoginTicket">
    <evaluate ="generateLoginTicketAction.generate(flowRequestContext)" />
    <transition on="generated" to="viewLoginForm" />
</action-state>
从CAS server debug信息和我的请求信息来看,server先生成这个ticket,返回给浏览器,当我们登录时,会带上这个ticket:

我登录时请求信息:

还是看看ticket怎么生成的吧,generateLoginTicketAction bean:

1
2
<bean id="generateLoginTicketAction" class="org.jasig.cas.web.flow.GenerateLoginTicketAction"
        p:ticketIdGenerator-ref="loginTicketUniqueIdGenerator"/>
/WEB-INF/spring-configuration/uniqueIdGenerators.xml定义了很多Generator, 比如上面的LoginTicketUniqueIdGenerator:
1
2
3
<bean id="loginTicketUniqueIdGenerator" class="org.jasig.cas.util.DefaultUniqueTicketIdGenerator">
    <constructor-arg index="0" type="int" value="30" />
</bean>
接着看GenerateLoginTicketAction的generate方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class GenerateLoginTicketAction {
    /** 3.5.1 - Login tickets SHOULD begin with characters "LT-" */
    private static final String PREFIX = "LT";
    @NotNull
    private UniqueTicketIdGenerator ticketIdGenerator;
    public final String generate(final RequestContext context) {
        final String loginTicket = this.ticketIdGenerator.getNewTicketId(PREFIX);//调用generator生成
        this.logger.debug("Generated login ticket " + loginTicket);
        WebUtils.putLoginTicket(context, loginTicket);//最终放到flowScope中
        return "generated";
    }
    ...
}

生成之后,就流向viewLoginForm state,其view未casLoginView,对应就是/WEB-INF/jsp/ui/default/casLoginView.jsp了:

1
2
3
4
5
6
7
8
9
10
11
12
<view-state id="viewLoginForm" view="casLoginView" model="credentials">
    <binder><!-- 绑定html form表单中的用户名及密码 -->
        <binding property="username" />
        <binding property="password" />
    </binder>
    <on-entry>
        <set name="viewScope.commandName" value="'credentials'" />
    </on-entry>
    <transition on="submit" bind="true" validate="true" to="realSubmit">
        <evaluate ="authenticationViaFormAction.doBind(flowRequestContext, flowScope.credentials)" />
    </transition>
</view-state>

于是就看到了CAS的登录界面:

对应的html表单内容大概是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<form id="fm1" class="fm-v clearfix" action="/cas/login" method="post">
    <h2>请输入您的用户名和密码.</h2>
    <div class="row fl-controls-left">
        <label for="username" class="fl-label">用户名:</label>                
        <input id="username" name="username" class="required" tabindex="1" accesskey="n" type="text" value                ="" size="25" autocomplete="false"/>
    </div>
    <div class="row fl-controls-left">
        <label for="password" class="fl-label">密 码:</label>
        <input id="password" name="password" class="required" tabindex="2" accesskey="p" type="password" v                alue="" size="25" autocomplete="off"/>
    </div>
    <div class="row check">
        <input id="warn" name="warn" value="true" tabindex="3" accesskey="w" type="checkbox" />
        <label for="warn">转向其他站点前提示我。</label>
    </div>
    <div class="row btn-row">
        <input type="hidden" name="lt" value="LT-5-rCdFkUxqSVKWTpzNgn2hLoZe9Fq0I2" /><!--生成的ticket-->
        <input type="hidden" name="execution" value="e1s1" />
     <input type="hidden" name="_eventId" value="submit" /> <!-- 对应提交到submit事件上-->
        <input class="btn-submit" name="submit" accesskey="l" value="登录" tabindex="4" type="submit" />
        <input class="btn-reset" name="reset" accesskey="c" value="重置" tabindex="5" type="reset" />
    </div>
</form>

当我们点击“登录”后,首先就到 authenticationViaFormAction.doBind(flowRequestContext, flowScope.credentials), authenticationViaFormAction在cas-servlet.xml中配置:

1
2
3
<bean id="authenticationViaFormAction" class="org.jasig.cas.web.flow.AuthenticationViaFormAction"
     p:centralAuthenticationService-ref="centralAuthenticationService"
     p:warnCookieGenerator-ref="warnCookieGenerator"/>

看doBind()方法:

1
2
3
4
5
6
7
public final void doBind(final RequestContext context, final Credentials credentials) throws Exception {
    final HttpServletRequest request = WebUtils.getHttpServletRequest(context);
    // 在authenticationViaFormAction bean定义中并没有注入credentialsBinder, 这里也不会做什么了
    if (this.credentialsBinder != null && this.credentialsBinder.supports(credentials.getClass())) {
       this.credentialsBinder.bind(request, credentials);
    }
}

接着看submit transition最终流向realSubmit:

1
2
3
4
5
6
7
8
9
10
11
12
<action-state id="realSubmit">
    <evaluate ="authenticationViaFormAction.submit(flowRequestContext, flowScope.credentials, messageContext)" />  
    <transition on="warn" to="warn" />
    <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>
看看authenticationViaFormAction的submit()方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public final String submit(final RequestContext context, final Credentials credentials, final MessageContext messageContext) throws Exception {
    // 首先验证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);
       }
    }
    try {
         WebUtils.putTicketGrantingTicketInRequestScope(context, this.centralAuthenticationService.createTicketGrantingTicket(credentials)); //这里会调用AuthenticationManagerImpl的authenticateAndObtainPricipal方法,该方法会依次调用我们在deployerConfigContext.xml中配置的authenticationManager bean的authenticationHandlers, 比如之前文章配置的数据库认证处理器等,验证成功了就会生成TGT(TicketGrantingTicket)返回给客户端。
putWarnCookieIfRequestParameterPresent(context);
return "success";
} catch (final TicketException e) {
         populateErrorsInstance(e, messageContext);
         if (isCauseAuthenticationException(e))
                return getAuthenticationExceptionEventId(e);
         return "error";
    }
}

假如我们登录成功了,flow继续流向sendTicketGrantingTicket state:

1
2
3
4
<action-state id="sendTicketGrantingTicket">
    <evaluate ="sendTicketGrantingTicketAction" />
    <transition to="serviceCheck" />
</action-state>
看看SendTicketGrantingTicketAction做了什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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);//将TGT作为Cookie加到Response中
       if (ticketGrantingTicketValueFromCookie != null && !ticketGrantingTicketId.equals(ticketGrantingTicketValueFromCookie)) {
           this.centralAuthenticationService
               .destroyTicketGrantingTicket(ticketGrantingTicketValueFromCookie);
       }
       return success();
   }

返回后,继续流向serviceCheck state, 会根据service是否为空来决定怎么流,也就是说,如果你是直接登录/cas/login, 那么就没有service属性,如果你是由其他客户端跳转过来登录的,那么service就是那个客户端跳转登录的url:

1
2
3
<decision-state id="serviceCheck">
    <if test="flowScope.service != null" then="generateServiceTicket" else="viewGenericLoginSuccess" />
</decision-state>

如果是直接登录的cas服务器,登录成功后,你就可以看到下面的界面:

我们假设是从你的另一个web client跳转过来的,那么就会流向generateServiceTicket:

1
2
3
4
5
6
<action-state id="generateServiceTicket">
    <evaluate ="generateServiceTicketAction" />
    <transition on="success" to ="warn" />
    <transition on="error" to="generateLoginTicket" />
    <transition on="gateway" to="gatewayServicesManagementCheck" />
</action-state>

看GenerateServiceTicketAction的doExecute方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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); //根据TGT生成service ticket
          WebUtils.putServiceTicketInRequestScope(context, serviceTicketId); //放到request中
          return success();
     } catch (final TicketException e) {
            if (isGatewayPresent(context)) {
                return result("gateway");
            }
     }
     return error();
}
之后,又流向warn state, warnCookieValue就是我们登录界面上是否勾选了提示复选框:

1
2
3
<decision-state id="warn">
    <if test="flowScope.warnCookieValue" then="showWarningView" else="redirect" />
</decision-state>

直接看redirect, 其主要构建Response对象,并放到requestScope中:

1
2
3
4
<action-state id="redirect">
        <evaluate ="flowScope.service.getResponse(requestScope.serviceTicketId)" result-type="org.jasig.cas.authentication.principal.Response" result="requestScope.response" />
        <transition to="postRedirectDecision" />
</action-state>
对于postRedirectDecision state,若是post过来的请求就到视图就到 /WEB-INF/view/ jsp /protocol/casPostResponseView.jsp ,若get则外部跳转到会之前的客户端url

1
2
3
<decision-state id="postRedirectDecision">
    <if test="requestScope.response.responseType.name() == 'POST'" then="postView" else="redirectView" />
</decision-state>

这就基本说了CAS服务整个登录怎么流动,下面也说说,我们客户端的处理流程。

-----------------------------------------------------------

web客户端主要的配置就在web.xml中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<listener>
    <listener-class>
        org.jasig.cas.client.session.SingleSignOutHttpSessionListener
    </listener-class>
    </listener>
    <filter>
        <filter-name>CasSingleSignOutFilter</filter-name>
     <filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>CasSingleSignOutFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <filter>
        <filter-name>CASFilter</filter-name>
        <filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>
        <init-param>
            <param-name>casServerLoginUrl</param-name>
            <param-value>https://localhost:8443/cas/login</param-value>
        </init-param>
        <init-param>
            <param-name>serverName</param-name>
            <param-value>http://localhost:8080</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>CASFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <filter>
        <filter-name>CasTicketFilter</filter-name>
        <filter-class>
            org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter</filter-class>
        <init-param>
            <param-name>casServerUrlPrefix</param-name>
            <param-value>https://localhost:8443/cas</param-value>
        </init-param>
        <init-param>
            <param-name>serverName</param-name>
            <param-value>http://localhost:8080</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>CasTicketFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <filter>
        <filter-name>CasRequestWrapFilter</filter-name>
        <filter-class>
            org.jasig.cas.client.util.HttpServletRequestWrapperFilter                                            </filter-class>
    </filter>
    <filter-mapping>
        <filter-name>CasRequestWrapFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <filter>
        <filter-name>AssertionThreadLocalFilter</filter-name>
        <filter-class>org.jasig.cas.client.util.AssertionThreadLocalFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>AssertionThreadLocalFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
SingleSignOutHttpSessionListener和SingleSignOutFilter用于登出操作。

CASFilter: 其doFilter方法实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public final void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException {
        final HttpServletRequest request = (HttpServletRequest) servletRequest;
        final HttpServletResponse response = (HttpServletResponse) servletResponse;
        final HttpSession session = request.getSession(false);
        final Assertion assertion = session != null (Assertion) session.getAttribute(CONST_CAS_ASSERTION) : null;
        if (assertion != null) { //有assertion信息(登录信息)就通过
            filterChain.doFilter(request, response);
            return;
        }
        final String serviceUrl = constructServiceUrl(request, response);//获取serviceUrl,即当前url
        final String ticket = CommonUtils.safeGetParameter(request,getArtifactParameterName());
        final boolean wasGatewayed = this.gatewayStorage.hasGatewayedAlready(request, serviceUrl);
        if (CommonUtils.isNotBlank(ticket) || wasGatewayed) { //如果有TGT就表示已登录过了
            filterChain.doFilter(request, response);
            return;
        }
        final String modifiedServiceUrl;
        if (this.gateway) {
            log.debug("setting gateway attribute in session");
            modifiedServiceUrl = this.gatewayStorage.storeGatewayInformation(request, serviceUrl);
        } else {
            modifiedServiceUrl = serviceUrl;
        }
        final String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl, getServiceParameterName(), modifiedServiceUrl, this.renew, this.gateway); //即将要跳转到CAS登录界面的url及其一些参数
        response.sendRedirect(urlToRedirectTo);
    }
其中urlToRedirectTo类似:

1
https://${cas-server-host}:port/cas/login?service=http%3A%2F%2Flocalhost%3A8080%2Fcas-web-client1%2Findex.jsp

经过跳转,然后登录成功后的请求信息:

登录成功以后我们再访问需要认证的url时,这时有了TGT, CAS服务端的login-webflow就有变化:

1
2
3
<decision-state id="ticketGrantingTicketExistsCheck">
    <if test="flowScope.ticketGrantingTicketId != null" then="hasServiceCheck" else="gatewayRequestCheck" />
</decision-state>
流向hasServiceCheck state:
1
2
3
<decision-state id="hasServiceCheck">
    <if test="flowScope.service != null" then="renewRequestCheck" else="viewGenericLoginSuccess" />
</decision-state>
接着流向renewRequestCheck state:
1
2
3
4
<decision-state id="renewRequestCheck">
    <if test="requestParameters.renew != '' and requestParameters.renew != null"
        then="serviceAuthorizationCheck" else="generateServiceTicket" />
</decision-state>
后面就和之前说的流程一样了。

当我们通过redirect返回之前的web客户端时,还会发生什么呢,这时有了TGT了,AuthenticationFilter中:

1
2
3
4
if (CommonUtils.isNotBlank(ticket) || wasGatewayed) { //有TGT通过
     filterChain.doFilter(request, response);
     return;
}

于是接着web客户端下一个的filter Cas20ProxyReceivingTicketValidationFilter:

1
2
3
4
5
6
7
8
9
10
11
12
13
<filter>
    <filter-name>CasTicketFilter</filter-name>
    <filter-class>
        org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter</filter-class>
    <init-param>
        <param-name>casServerUrlPrefix</param-name>
        <param-value>https://localhost:8443/cas</param-value>
    </init-param>
    <init-param>
        <param-name>serverName</param-name>
        <param-value>http://localhost:8080</param-value>
    </init-param>
</filter>

Cas20ProxyReceivingTicketValidationFilter过滤处理主要是其父类AbstractTicketValidationFilter实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public final void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException {
  //子类预处理,Cas20ProxyReceivingTicketValidationFliter做了一些处理
  if (!preFilter(servletRequest, servletResponse, filterChain)) {
     return;
  }
  final HttpServletRequest request = (HttpServletRequest) servletRequest;
  final HttpServletResponse response = (HttpServletResponse) servletResponse;
  final String ticket = CommonUtils.safeGetParameter(request, getArtifactParameterName());//获取ticket
  if (CommonUtils.isNotBlank(ticket)) {
     try {
      final Assertion assertion = this.ticketValidator.validate(ticket, constructServiceUrl(request, response));//再次拿ticket到服务端验证,看是否确实存在,或者是否过期, 默认实现为Cas20ProxyTicketValidator
      request.setAttribute(CONST_CAS_ASSERTION, assertion);
      if (this.useSession) {//Aseesion放到session中,所以你就知道怎么在我们应用中访问登录的用户信息了
          request.getSession().setAttribute(CONST_CAS_ASSERTION, assertion);
      }
      onSuccessfulValidation(request, response, assertion);
      if (this.redirectAfterValidation) { // 默认true
          log. debug("Redirecting after successful ticket validation.");
          response.sendRedirect(constructServiceUrl(request, response));
          return;
      }
    }catch (final TicketValidationException e) {
          response.setStatus(HttpServletResponse.SC_FORBIDDEN);
          onFailedValidation(request, response);
          if (this.exceptionOnValidationFailure) {
              throw new ServletException(e);
          }
          return;
    }
  }
  filterChain.doFilter(request, response);
}

validate方法由AbstractBasedTicketValidator实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public Assertion validate(final String ticket, final String service) throws TicketValidationException {
      //获取验证url, 类似<span><span style="line-height:24px;background-color:#F8FBFC;">https:${cas-server-host}:port/cas/serviceValidate?ticket=xxx&service=yyy</span></span> final String validationUrl = constructValidationUrl(ticket, service);
      if (log.isDebugEnabled()) {
            log.debug("Constructing validation url: " + validationUrl);
      }
      try {
            //发送请求并获取返回内容(通过java URLConnection发送请求,直接读取Response输入流)
            final String serverResponse = retrieveResponseFromServer(new URL(validationUrl), ticket);                                                                                           
            if (serverResponse == null) {
                throw new TicketValidationException("The CAS server returned no response.");
            }
             
            if (log.isDebugEnabled()) {
                log.debug("Server response: " + serverResponse);
            }
            //解析CAS服务端返回的内容为Assertion对象
            return parseResponseFromServer(serverResponse);
      } catch (final MalformedURLException e) {
            throw new TicketValidationException(e);
    }
}

上面发送认证请求后的返回内容类似:

1
2
3
4
5
<cas:serviceResponse xmlns:cas='http://www./tp/cas'>
    <cas:authenticationSuccess>
        <cas:user>admin</cas:user>
    </cas:authenticationSuccess>
</cas:serviceResponse>

验证请求/cas/serviceValidate则对应服务器端配置的SafeDispatcherServlet:

这个Servlet中包含有一个我们熟悉的Spring-MVC的前端分发器DispatcherServlet, 明显由它来奋发我们的请求,那么/validateService对应那个Controller呢?看cas-servlet.xml配置:

看ServiceValidateController的handleRequestInternal方法重要的一句:

1
final Assertion assertion = this.centralAuthenticationService.validateServiceTicket(serviceTicketId, service);

就是根据CentralAuthenticationServiceImpl的下面两个变量来验证:

1
2
3
4
5
6
/** TicketRegistry for storing and retrieving tickets as needed. */
@NotNull
private TicketRegistry ticketRegistry;
/** New Ticket Registry for storing and retrieving services tickets. Can point to the same one as the ticketRegistry variable. */
@NotNull
private TicketRegistry serviceTicketRegistry;

整个登录基本流程简单的了解over.

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约