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.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.

转载于:https://my.oschina.net/psuyun/blog/402165

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Java 可以通过使用 java.util.concurrent 包中的 AtomicReference 类来实现 compare-and-swap (CAS) 算法。AtomicReference 类是一个原子性的引用类型,允许您在线程安全的方式更新和读取数据。 以下是实现单点登录的代码示例: ``` import java.util.concurrent.atomic.AtomicReference; public class SingleSignOn { private static final AtomicReference<String> SSO_TOKEN = new AtomicReference<>(); public static String getToken() { return SSO_TOKEN.get(); } public static boolean login(String username, String password) { // 实际上需要使用用户名和密码进行验证 if (username.equals("admin") && password.equals("password")) { String token = generateToken(username); return SSO_TOKEN.compareAndSet(null, token); } return false; } private static String generateToken(String username) { return username + System.currentTimeMillis(); } } ``` 在上面的代码中,我们使用了 AtomicReference 类来维护单点登录令牌。如果令牌为空,则说明用户尚未登录,可以通过使用 compareAndSet 方法来尝试登录。如果登录成功,则令牌将被设置为生成的令牌值;否则,令牌将保持不变。 希望这段代码对您有所帮助! ### 回答2: CAS(Central Authentication Service)是一种单点登录(SSO)协议,用于集中管理多个应用系统的用户身份认证。下面是用Java实现CAS单点登录的简要流程: 1. 配置CAS服务端: 首先,在服务器上搭建CAS服务端。一般使用Java开发的CAS服务器有一些成熟的开源实现,如Apereo CASJasig CAS等。配置CAS服务端包括定义认证源(如数据库、LDAP等)、配置认证策略、绑定或生成SSL证书,以确保安全性。 2. 配置应用系统: 在需要实现CAS单点登录的应用系统中,配置CAS客户端。CAS客户端可以使用Java CAS客户端或其他遵循CAS协议的客户端库。配置CAS客户端主要包括指定CAS服务端地址、配置回调URL和授权过滤规则。 3. 用户认证: 当用户访问某个应用系统时,该系统会检测用户是否已经登录。如果未登录,则会重定向至CAS服务端登录页面。 4. CAS服务端认证: 用户在CAS服务端登录页面输入用户名和密码后,CAS服务端会对用户进行认证。认证成功后,CAS服务端会生成一个全局的授权票据(Ticket),并重定向至应用系统的回调URL,并将授权票据作为参数传递给该URL。 5. 应用系统认证: 应用系统接收到来自CAS服务端的回调请求后,会通过与CAS服务端的通信,验证授权票据的有效性。验证成功后,将在应用系统中建立本地会话,并标记该用户已登录。 6. 单点登出: CAS单点登录还支持单点登出功能。用户在任何一个应用系统中登出后,会通知CAS服务端登出,CAS服务端再通知其他已经登录的应用系统进行登出操作。 通过以上流程,就可以实现CAS单点登录。此外,CAS还提供了一些其他的功能,如多因素认证、代理认证等,以满足不同场景的需求。 ### 回答3: CAS(Central Authentication Service)是一种单点登录协议,用于实现在多个应用程序之间的用户身份认证和授权。下面是使用Java实现CAS单点登录的步骤: 1. 搭建CAS Server 首先,需要搭建一个CAS Server作为用户认证和授权的中心。可以使用开源的CAS Server,例如Apereo CASJasig CAS。搭建CAS Server需要配置认证源和授权策略,并生成CAS的服务端证书。 2. 配置CAS Client 每个应用程序都需要配置CAS Client以与CAS Server进行通信。首先,需要引入CAS Client的Java库,然后在应用程序的配置文件中配置CAS Client的相关参数,如CAS Server的地址、服务端证书等。 3. 实现登录功能 当用户访问应用程序时,应用程序会将用户重定向到CAS Server的登录页面。用户在CAS Server上进行身份认证后,CAS Server会生成一个CAS Ticket,并将其包含在重定向URL中返回给应用程序。 4. 验证CAS Ticket 应用程序在接收到CAS Ticket后,通过发送POST请求到CAS Server的验证接口来验证CAS Ticket的有效性。CAS Server会验证Ticket的签名和有效期,并返回验证结果给应用程序。 5. 维护登录状态 如果验证结果为有效,应用程序会保存用户的登录状态。可以使用会话管理技术(如Session)来维护用户的登录状态,并在需要时使用会话中的用户信息进行权限控制。 6. 单点注销 当用户注销时,应用程序发送注销请求到CAS Server的注销接口。CAS Server会销毁用户的会话和Token信息,并向所有已登录的应用程序发送注销通知,以保证单点注销的效果。 通过以上步骤,就可以实现基于Java的CAS单点登录。使用CAS协议,可以实现用户只需登录一次,即可进行多个应用程序的访问和授权,提高了用户体验和安全性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值