之前文章讲到了怎么利用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>
|
1
|
<var
name="credentials"
class="org.jasig.cas.authentication.principal.UsernamePasswordCredentials"
/>
|
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"
/>
|
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>
|
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>
|
1
2
3
4
|
<action-state
id="serviceAuthorizationCheck">
<evaluate
="serviceAuthorizationCheck"/>
<transition
to="generateLoginTicket"/>
</action-state>
|
1
2
3
4
|
<action-state
id="generateLoginTicket">
<evaluate
="generateLoginTicketAction.generate(flowRequestContext)"
/>
<transition
on="generated"
to="viewLoginForm"
/>
</action-state>
|
我登录时请求信息:
还是看看ticket怎么生成的吧,generateLoginTicketAction bean:
1
2
|
<bean
id="generateLoginTicketAction"
class="org.jasig.cas.web.flow.GenerateLoginTicketAction"
p:ticketIdGenerator-ref="loginTicketUniqueIdGenerator"/>
|
1
2
3
|
<bean
id="loginTicketUniqueIdGenerator"
class="org.jasig.cas.util.DefaultUniqueTicketIdGenerator">
<constructor-arg
index="0"
type="int"
value="30"
/>
</bean>
|
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>
|
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>
|
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();
}
|
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>
|
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>
|
看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);
}
|
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>
|
1
2
3
|
<decision-state
id="hasServiceCheck">
<if
test="flowScope.service != null"
then="renewRequestCheck"
else="viewGenericLoginSuccess"
/>
</decision-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. */
@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.