一.前期準(zhǔn)備
1.cas源碼版本
<version>4.1.11-SNAPSHOT</version>
2.服務(wù)端
http://www.eric.cas.server.com:8080/cas-server
3.cas client
http://www.eric.cas.client.com:8081/cas/index.do
4.cas client web.xml配置
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<display-name>Archetype Created Web Application</display-name>
<servlet>
<servlet-name>cas-client-4.x</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>cas-client-4.x</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath*:/applicationContext.xml
</param-value>
</context-param>
<!-- ****************** 單點(diǎn)登錄開始 ********************-->
<!-- 用于實(shí)現(xiàn)單點(diǎn)登出功能 可選 -->
<listener>
<listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>
</listener>
<filter>
<filter-name>CAS Single Sign Out Filter</filter-name>
<filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>
<init-param>
<param-name>casServerUrlPrefix</param-name>
<param-value>http://www.eric.cas.server.com:8080/</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CAS Single Sign Out Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 該過濾器負(fù)責(zé)用戶的認(rèn)證工作危纫,必須 -->
<filter>
<filter-name>CAS Authentication Filter</filter-name>
<filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>
<init-param>
<param-name>casServerLoginUrl</param-name>
<!--casServerLoginUrl:cas服務(wù)的登陸url -->
<param-value>http://www.eric.cas.server.com:8080/login</param-value>
</init-param>
<init-param>
<!--serverName:本項(xiàng)目的ip+port -->
<param-name>serverName</param-name>
<param-value>http://www.eric.cas.client.com:8081</param-value>
</init-param>
<init-param>
<param-name>useSession</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>redirectAfterValidation</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CAS Authentication Filter</filter-name>
<url-pattern>/cas/*</url-pattern>
</filter-mapping>
<!-- 該過濾器負(fù)責(zé)對(duì)Ticket的校驗(yàn)工作,必須-->
<filter>
<filter-name>CAS Validation Filter</filter-name>
<filter-class>org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter</filter-class>
<init-param>
<param-name>casServerUrlPrefix</param-name>
<param-value>http://www.eric.cas.server.com:8080/</param-value>
</init-param>
<init-param>
<param-name>serverName</param-name>
<param-value>http://www.eric.cas.client.com:8081</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CAS Validation Filter</filter-name>
<!-- 對(duì)cas做登錄攔截-->
<url-pattern>/cas/*</url-pattern>
</filter-mapping>
<!-- 該過濾器對(duì)HttpServletRequest請(qǐng)求包裝, 可通過HttpServletRequest的getRemoteUser()方法獲得登錄用戶的登錄名祸轮,可選 -->
<filter>
<filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
<filter-class>org.jasig.cas.client.util.HttpServletRequestWrapperFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 該過濾器使得可以通過org.jasig.cas.client.util.AssertionHolder來獲取用戶的登錄名茁计。
比如AssertionHolder.getAssertion().getPrincipal().getName()沃暗。
這個(gè)類把Assertion信息放在ThreadLocal變量中冤馏,這樣應(yīng)用程序不在web層也能夠獲取到當(dāng)前登錄信息 -->
<filter>
<filter-name>CAS Assertion Thread Local Filter</filter-name>
<filter-class>org.jasig.cas.client.util.AssertionThreadLocalFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CAS Assertion Thread Local Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- ****************** 單點(diǎn)登錄結(jié)束 ********************-->
</web-app>
登錄流程
當(dāng)從瀏覽器訪問配置了單點(diǎn)登錄的應(yīng)用系統(tǒng)時(shí)(http://www.eric.cas.client.com:8081/cas/index.do),
請(qǐng)求第一次到達(dá)SingleSignOutFilter過濾器
一. SingleSignOutFilter
SingleSignOutFilter的doFilter方法如下:
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;
if(!this.handlerInitialized.getAndSet(true)) {
HANDLER.init();
}
// 處理請(qǐng)求方法
if(HANDLER.process(request, response)) {
filterChain.doFilter(servletRequest, servletResponse);
}
}
可以看到SingleSignOutFilter中處理請(qǐng)求的主要方法是
HANDLER.process(request, response)
實(shí)際調(diào)用的是SingleSignOutHandler的process方法
public boolean process(HttpServletRequest request, HttpServletResponse response) {
// 是否是帶有token參數(shù)的請(qǐng)求 如ticket參數(shù)
if(this.isTokenRequest(request)) {
this.logger.trace("Received a token request");
// 記錄session
this.recordSession(request);
return true;
} else if(this.isBackChannelLogoutRequest(request)) {
// 登出請(qǐng)求 銷毀session
this.logger.trace("Received a back channel logout request");
this.destroySession(request);
return false;
} else if(this.isFrontChannelLogoutRequest(request)) {
this.logger.trace("Received a front channel logout request");
// 登出請(qǐng)求 銷毀session
this.destroySession(request);
String redirectionUrl = this.computeRedirectionToServer(request);
if(redirectionUrl != null) {
CommonUtils.sendRedirect(response, redirectionUrl);
}
return false;
} else {
this.logger.trace("Ignoring URI for logout: {}", request.getRequestURI());
return true;
}
}
以上源碼可以看到典徊,process方法主要是用于單點(diǎn)登錄時(shí)記錄登錄的session信息,單點(diǎn)登出時(shí),銷毀記錄的session内列。
由于是第一次請(qǐng)求撵术,直接返回true,跳轉(zhuǎn)到下一個(gè)過濾器AuthenticationFilter
二. AuthenticationFilter
AuthenticationFilter的doFilter方法如下:
public final void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;
if(this.isRequestUrlExcluded(request)) {
this.logger.debug("Request is ignored.");
filterChain.doFilter(request, response);
} else {
//判斷登錄session是否存在
HttpSession session = request.getSession(false);
//從session中獲取名為"_const_cas_assertion_"的Assertion
Assertion assertion = session != null?(Assertion)session.getAttribute("_const_cas_assertion_"):null;
if(assertion != null) {
//登錄session存在 則執(zhí)行下一個(gè)filter
filterChain.doFilter(request, response);
} else {
String serviceUrl = this.constructServiceUrl(request, response);
String ticket = this.retrieveTicketFromRequest(request);
boolean wasGatewayed = this.gateway && this.gatewayStorage.hasGatewayedAlready(request, serviceUrl);
if(!CommonUtils.isNotBlank(ticket) && !wasGatewayed) {
this.logger.debug("no ticket and no assertion found");
String modifiedServiceUrl;
if(this.gateway) {
this.logger.debug("setting gateway attribute in session");
modifiedServiceUrl = this.gatewayStorage.storeGatewayInformation(request, serviceUrl);
} else {
modifiedServiceUrl = serviceUrl;
}
this.logger.debug("Constructed service url: {}", modifiedServiceUrl);
String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl, this.getServiceParameterName(), modifiedServiceUrl, this.renew, this.gateway);
this.logger.debug("redirecting to \"{}\"", urlToRedirectTo);
this.authenticationRedirectStrategy.redirect(request, response, urlToRedirectTo);
} else {
//如果ticket不為空,本過濾器處理完成话瞧,處理下個(gè)過濾器
filterChain.doFilter(request, response);
}
}
}
}
由以上源碼可知嫩与,當(dāng)請(qǐng)求到達(dá)之后執(zhí)行以下操作:
- 首先判斷當(dāng)前請(qǐng)求是否需要過濾,不需要?jiǎng)t直接跳轉(zhuǎn)到下一個(gè)過濾器交排。
- 從session中獲取名為“const_cas_assertion”的assertion對(duì)象划滋,判斷assertion是否存在,如果存在埃篓,說明已經(jīng)登錄处坪,執(zhí)行下一個(gè)過濾器。如果不存在架专,執(zhí)行第3步稻薇。
- 生成serviceUrl(http://www.eric.cas.client.com:8081/cas/index.do),并且從request中獲取票據(jù)參數(shù)ticket胶征,判斷ticket是否為空塞椎,如果不為空?qǐng)?zhí)行下一個(gè)過濾器。如果為空睛低,執(zhí)行第4步案狠。
- 生成重定向URL(http://www.eric.cas.server.com:8080/cas-server/login?service=http://www.eric.cas.client.com:8081/cas/index.do)。
- 執(zhí)行重定向钱雷,跳轉(zhuǎn)到單點(diǎn)登錄服務(wù)器骂铁,顯示登錄頁(yè)面
瀏覽器登錄頁(yè)面
- 登錄頁(yè)面輸入用戶名和密碼,執(zhí)行登錄操作罩抗,請(qǐng)求到達(dá)cas server,cas server做登錄校驗(yàn)之后拉庵,生成登錄所需要的ticket并重定向到cas client,具體重定向地址如下
http://www.eric.cas.client.com:8081/cas/index.do?ticket=ST-1-alt4ccCXxjOamzWU4Hid-cas01.example.org
此時(shí)已經(jīng)拿到了單點(diǎn)登錄的臨時(shí)票據(jù)Service Ticket套蒂,簡(jiǎn)稱ST钞支。
帶有ST參數(shù)的請(qǐng)求重新到達(dá)cas client,還是先到達(dá)SingleSignOutFilter,此時(shí)已經(jīng)拿到登錄的ST,則保存登錄的票據(jù)信息到session操刀,然后執(zhí)行下一個(gè)攔截器AuthenticationFilter
if(this.isTokenRequest(request)) {
this.logger.trace("Received a token request");
this.recordSession(request);
return true;
}
private void recordSession(HttpServletRequest request) {
HttpSession session = request.getSession(this.eagerlyCreateSessions);
if(session == null) {
this.logger.debug("No session currently exists (and none created). Cannot record session information for single sign out.");
} else {
String token = CommonUtils.safeGetParameter(request, this.artifactParameterName, this.safeParameters);
this.logger.debug("Recording session for token {}", token);
try {
this.sessionMappingStorage.removeBySessionById(session.getId());
} catch (Exception var5) {
;
}
this.sessionMappingStorage.addSessionById(token, session);
}
}
帶有ST參數(shù)的請(qǐng)求到達(dá)AuthenticationFilter,此時(shí)assertion對(duì)象仍然為空但是ticket不為空烁挟,則直接跳轉(zhuǎn)到下一個(gè)過濾器Cas20ProxyReceivingTicketValidationFilter
三. Cas20ProxyReceivingTicketValidationFilter
Cas20ProxyReceivingTicketValidationFilter繼承了AbstractTicketValidationFilter類,doFilter方法如下
public final void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
if(this.preFilter(servletRequest, servletResponse, filterChain)) {
HttpServletRequest request = (HttpServletRequest)servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;
/從request中獲取ST參數(shù)
String ticket = this.retrieveTicketFromRequest(request);
if(CommonUtils.isNotBlank(ticket)) {
this.logger.debug("Attempting to validate ticket: {}", ticket);
try {
//驗(yàn)證ticket并產(chǎn)生Assertion對(duì)象骨坑,錯(cuò)誤拋出TicketValidationException異常
Assertion e = this.ticketValidator.validate(ticket, this.constructServiceUrl(request, response));
this.logger.debug("Successfully authenticated user: {}", e.getPrincipal().getName());
//驗(yàn)證成功 保存認(rèn)證用戶信息到session
request.setAttribute("_const_cas_assertion_", e);
if(this.useSession) {
request.getSession().setAttribute("_const_cas_assertion_", e);
}
this.onSuccessfulValidation(request, response, e);
//跳轉(zhuǎn)到請(qǐng)求地址
if(this.redirectAfterValidation) {
this.logger.debug("Redirecting after successful ticket validation.");
response.sendRedirect(this.constructServiceUrl(request, response));
return;
}
} catch (TicketValidationException var8) {
this.logger.debug(var8.getMessage(), var8);
this.onFailedValidation(request, response);
if(this.exceptionOnValidationFailure) {
throw new ServletException(var8);
}
response.sendError(403, var8.getMessage());
return;
}
}
filterChain.doFilter(request, response);
}
}
Cas20ProxyReceivingTicketValidationFilter撼嗓,執(zhí)行以下操作:
從request獲取ticket參數(shù),如果ticket為空,繼續(xù)處理下一個(gè)過濾器且警。如果參數(shù)不為空粉捻,驗(yàn)證ticket參數(shù)的合法性。
驗(yàn)證ticket斑芜,TicketValidator的validate方法通過httpClient訪問CAS服務(wù)器端
http://www.eric.cas.server.com:8080/serviceValidate?ticket=ST-6-okgjpFYrNhfVYbCx9xYQ-cas01.example.org&service=http%3A%2F%2Fwww.eric.cas.client.com%3A8081%2Fcas%2Findex.do
驗(yàn)證ticket是否正確杀迹,并返回assertion對(duì)象。
如果驗(yàn)證失敗押搪,拋出異常,跳轉(zhuǎn)到錯(cuò)誤頁(yè)面浅碾。
如果驗(yàn)證成功大州,則將Assertion對(duì)象保存到session中,s垂谢,繼續(xù)處理下一個(gè)過濾器厦画。
request.getSession().setAttribute("_const_cas_assertion_", e);
單點(diǎn)登錄流程結(jié)束
服務(wù)端具體驗(yàn)證ST流程
補(bǔ)充上面說的cas client請(qǐng)求cas server驗(yàn)證ticket的具體邏輯
驗(yàn)證請(qǐng)求如下:
http://www.eric.cas.server.com:8080/serviceValidate?ticket=ST-6-okgjpFYrNhfVYbCx9xYQ-cas01.example.org&service=http%3A%2F%2Fwww.eric.cas.client.com%3A8081%2Fcas%2Findex.do
查看服務(wù)端cas-servlet.xml文件配置
<bean
id="handlerMappingC"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping"
p:alwaysUseFullPath="true">
<property name="mappings">
<util:properties>
<prop key="/serviceValidate">serviceValidateController</prop>
<prop key="/proxyValidate">proxyValidateController</prop>
<prop key="/p3/serviceValidate">v3ServiceValidateController</prop>
<prop key="/p3/proxyValidate">v3ProxyValidateController</prop>
<prop key="/validate">legacyValidateController</prop>
<prop key="/proxy">proxyController</prop>
<prop key="/authorizationFailure.html">passThroughController</prop>
</util:properties>
</property>
</bean>
可以看到cas client發(fā)送的ST驗(yàn)證請(qǐng)求由serviceValidateController負(fù)責(zé)處理
@Override
protected final ModelAndView handleRequestInternal(final HttpServletRequest request, final HttpServletResponse response)
throws Exception {
final WebApplicationService service = this.argumentExtractor.extractService(request);
final String serviceTicketId = service != null ? service.getArtifactId() : null;
if (service == null || serviceTicketId == null) {
logger.debug("Could not identify service and/or service ticket for service: [{}]", service);
return generateErrorView(CasProtocolConstants.ERROR_CODE_INVALID_REQUEST,
CasProtocolConstants.ERROR_CODE_INVALID_REQUEST, null);
}
try {
final Credential serviceCredential = getServiceCredentialsFromRequest(service, request);
TicketGrantingTicket proxyGrantingTicketId = null;
if (serviceCredential != null) {
try {
proxyGrantingTicketId = this.centralAuthenticationService.delegateTicketGrantingTicket(serviceTicketId,
serviceCredential);
logger.debug("Generated PGT [{}] off of service ticket [{}] and credential [{}]",
proxyGrantingTicketId.getId(), serviceTicketId, serviceCredential);
} catch (final AuthenticationException e) {
logger.info("Failed to authenticate service credential {}", serviceCredential);
} catch (final TicketException e) {
logger.error("Failed to create proxy granting ticket for {}", serviceCredential, e);
}
if (proxyGrantingTicketId == null) {
return generateErrorView(CasProtocolConstants.ERROR_CODE_INVALID_PROXY_CALLBACK,
CasProtocolConstants.ERROR_CODE_INVALID_PROXY_CALLBACK,
new Object[] {serviceCredential.getId()});
}
}
// 驗(yàn)證ST是否正確 并且生成Assertion對(duì)象
final Assertion assertion = this.centralAuthenticationService.validateServiceTicket(serviceTicketId, service);
final ValidationSpecification validationSpecification = this.getCommandClass();
final ServletRequestDataBinder binder = new ServletRequestDataBinder(validationSpecification, "validationSpecification");
initBinder(request, binder);
binder.bind(request);
if (!validationSpecification.isSatisfiedBy(assertion)) {
logger.debug("Service ticket [{}] does not satisfy validation specification.", serviceTicketId);
return generateErrorView(CasProtocolConstants.ERROR_CODE_INVALID_TICKET,
CasProtocolConstants.ERROR_CODE_INVALID_TICKET, null);
}
String proxyIou = null;
if (serviceCredential != null && this.proxyHandler.canHandle(serviceCredential)) {
proxyIou = this.proxyHandler.handle(serviceCredential, proxyGrantingTicketId);
if (StringUtils.isEmpty(proxyIou)) {
return generateErrorView(CasProtocolConstants.ERROR_CODE_INVALID_PROXY_CALLBACK,
CasProtocolConstants.ERROR_CODE_INVALID_PROXY_CALLBACK,
new Object[] {serviceCredential.getId()});
}
}
onSuccessfulValidation(serviceTicketId, assertion);
logger.debug("Successfully validated service ticket {} for service [{}]", serviceTicketId, service.getId());
return generateSuccessView(assertion, proxyIou, service, proxyGrantingTicketId);
} catch (final TicketValidationException e) {
final String code = e.getCode();
return generateErrorView(code, code,
new Object[] {serviceTicketId, e.getOriginalService().getId(), service.getId()});
} catch (final TicketException te) {
return generateErrorView(te.getCode(), te.getCode(),
new Object[] {serviceTicketId});
} catch (final UnauthorizedProxyingException e) {
return generateErrorView(e.getMessage(), e.getMessage(), new Object[] {service.getId()});
} catch (final UnauthorizedServiceException e) {
return generateErrorView(e.getMessage(), e.getMessage(), null);
}
}
查看ST驗(yàn)證的主要方法CentralAuthenticationServiceImpl類的validateServiceTicket方法
public Assertion validateServiceTicket(final String serviceTicketId, final Service service) throws TicketException {
final RegisteredService registeredService = this.servicesManager.findServiceBy(service);
verifyRegisteredServiceProperties(registeredService, service);
// 從緩存中查看ST是否存在
final ServiceTicket serviceTicket = this.ticketRegistry.getTicket(serviceTicketId, ServiceTicket.class);
if (serviceTicket == null) {
logger.info("Service ticket [{}] does not exist.", serviceTicketId);
throw new InvalidTicketException(serviceTicketId);
}
try {
synchronized (serviceTicket) {
// ST是否過期
if (serviceTicket.isExpired()) {
logger.info("ServiceTicket [{}] has expired.", serviceTicketId);
throw new InvalidTicketException(serviceTicketId);
}
// ST是否合法
if (!serviceTicket.isValidFor(service)) {
logger.error("Service ticket [{}] with service [{}] does not match supplied service [{}]",
serviceTicketId, serviceTicket.getService().getId(), service);
throw new UnrecognizableServiceForServiceTicketValidationException(serviceTicket.getService());
}
}
final TicketGrantingTicket root = serviceTicket.getGrantingTicket().getRoot();
final Authentication authentication = getAuthenticationSatisfiedByPolicy(
root, new ServiceContext(serviceTicket.getService(), registeredService));
final Principal principal = authentication.getPrincipal();
final AttributeReleasePolicy attributePolicy = registeredService.getAttributeReleasePolicy();
logger.debug("Attribute policy [{}] is associated with service [{}]", attributePolicy, registeredService);
@SuppressWarnings("unchecked")
final Map<String, Object> attributesToRelease = attributePolicy != null
? attributePolicy.getAttributes(principal) : Collections.EMPTY_MAP;
final String principalId = registeredService.getUsernameAttributeProvider().resolveUsername(principal, service);
final Principal modifiedPrincipal = this.principalFactory.createPrincipal(principalId, attributesToRelease);
final AuthenticationBuilder builder = DefaultAuthenticationBuilder.newInstance(authentication);
builder.setPrincipal(modifiedPrincipal);
return new ImmutableAssertion(
builder.build(),
serviceTicket.getGrantingTicket().getChainedAuthentications(),
serviceTicket.getService(),
serviceTicket.isFromNewLogin());
} finally {
if (serviceTicket.isExpired()) {
this.ticketRegistry.deleteTicket(serviceTicketId);
}
}
}
這里主要看下serviceTicket.isValidFor(service)方法
@Override
public boolean isValidFor(final Service serviceToValidate) {
updateState();
return serviceToValidate.matches(this.service);
}
該方法實(shí)際調(diào)用的是AbstractWebApplicationService類的matches方法
@Override
public boolean matches(final Service service) {
try {
final String thisUrl = URLDecoder.decode(this.id, "UTF-8");
final String serviceUrl = URLDecoder.decode(service.getId(), "UTF-8");
logger.trace("Decoded urls and comparing [{}] with [{}]", thisUrl, serviceUrl);
return thisUrl.equalsIgnoreCase(serviceUrl);
} catch (final Exception e) {
logger.error(e.getMessage(), e);
}
return false;
}
可以看到驗(yàn)證ST就是驗(yàn)證的登錄請(qǐng)求保存的service的ID和驗(yàn)證請(qǐng)求的service的ID是否相等,相等就認(rèn)為ST合法滥朱。
這里也可以修改驗(yàn)證邏輯根暑,添加自己的驗(yàn)證邏輯,但前提是確保ST驗(yàn)證的合法性徙邻,安全性排嫌。
這里的Service到底是什么呢,其實(shí)Service就是cas server把收到的客戶端請(qǐng)求封裝之后產(chǎn)生的對(duì)象
具體配置文件是argumentExtractorsConfiguration.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:util="http://www.springframework.org/schema/util"
xmlns="http://www.springframework.org/schema/beans"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">
<description>
Argument Extractors are what are used to translate HTTP requests into requests of the appropriate protocol (i.e.
CAS, SAML, SAML2,
OpenId, etc.). By default, only CAS is enabled.
</description>
<!--
<bean id="samlArgumentExtractor" class="org.jasig.cas.support.saml.web.support.SamlArgumentExtractor" />
-->
<bean id="casArgumentExtractor" class="org.jasig.cas.web.support.CasArgumentExtractor"/>
<util:list id="argumentExtractors">
<!-- <ref bean="samlArgumentExtractor" /> -->
<ref bean="casArgumentExtractor"/>
</util:list>
</beans>
CasArgumentExtractor類
public final class CasArgumentExtractor extends AbstractArgumentExtractor {
@Override
public WebApplicationService extractServiceInternal(final HttpServletRequest request) {
return SimpleWebApplicationServiceImpl.createServiceFrom(request);
}
}
可以看到Service接口的實(shí)例是SimpleWebApplicationServiceImpl類的對(duì)象
/**
* Creates the service from the request.
*
* @param request the request
* @return the simple web application service impl
*/
public static SimpleWebApplicationServiceImpl createServiceFrom(
final HttpServletRequest request) {
final String targetService = request.getParameter(CONST_PARAM_TARGET_SERVICE);
final String service = request.getParameter(CONST_PARAM_SERVICE);
final String serviceAttribute = (String) request.getAttribute(CONST_PARAM_SERVICE);
final String method = request.getParameter(CONST_PARAM_METHOD);
final String serviceToUse;
if (StringUtils.hasText(targetService)) {
serviceToUse = targetService;
} else if (StringUtils.hasText(service)) {
serviceToUse = service;
} else {
serviceToUse = serviceAttribute;
}
if (!StringUtils.hasText(serviceToUse)) {
return null;
}
final String id = cleanupUrl(serviceToUse);
final String artifactId = request.getParameter(CONST_PARAM_TICKET);
return new SimpleWebApplicationServiceImpl(id, serviceToUse,
artifactId, "POST".equals(method) ? Response.ResponseType.POST
: Response.ResponseType.REDIRECT);
}
這里可以看到service的封裝過程,由此可見登錄時(shí)傳遞給cas server的service參數(shù)和ST驗(yàn)證時(shí)傳遞給cas server的service參數(shù)必須相同缰犁,ST才能驗(yàn)證成功
服務(wù)端驗(yàn)證ST流程結(jié)束淳地。