jasig CAS登录验证分析
jasig CAS登录验证分析
彭苏云 发表于3年前
jasig CAS登录验证分析
  • 发表于 3年前
  • 阅读 89
  • 收藏 2
  • 点赞 0
  • 评论 0

腾讯云 十分钟定制你的第一个小程序>>>   

之前文章讲到了怎么利用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.springframework.org/schema/webflow"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://www.springframework.org/schema/webflow
                          http://www.springframework.org/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)  throwsIOException, 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)  throwsIOException, 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.yale.edu/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. */
private  TicketRegistry ticketRegistry;
/** New Ticket Registry for storing and retrieving services tickets. Can point to the same one as the ticketRegistry variable. */
private  TicketRegistry serviceTicketRegistry;

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

共有 人打赏支持
粉丝 42
博文 202
码字总数 54255
×
彭苏云
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
* 金额(元)
¥1 ¥5 ¥10 ¥20 其他金额
打赏人
留言
* 支付类型
微信扫码支付
打赏金额:
已支付成功
打赏金额: