此版本的源碼基於cas5.3,源碼鏈接:https://github.com/apereo/cas/tree/5.3.x
首先準備好客戶端和服務端,整個訪問的流程參見 https://apereo.github.io/cas/5.2.x/protocol/CAS-Protocol.html
整個流程的狀態變化:
服務端系統啓動—客戶端發起登陸請求—服務端校驗請求—展示登陸頁—服務端發起登陸請求—校驗登陸—生成TGT、ST令牌—重定向到客戶端—客戶端發起校驗請求—服務端校驗並返回用戶信息。
下面就從以上的幾個大步驟簡單解析下,本文着重關注服務端的流程,客戶端的流程可以參見: https://my.oschina.net/woniuyi/blog/4454460
1. 系統啓動
這個階段主要關注一些重要的配置類加載以及登陸流程的定義。
- CasSupportActionsConfiguration
啓動的時候會初始化系列action,這些action就是真正在運行時候處理請求的,比較典型的action如下:action名稱-實際類型
authenticationViaFormAction-InitialAuthenticationAction 認證請求 serviceAuthorizationCheck-ServiceAuthorizationCheck 客戶端校驗 sendTicketGrantingTicketAction-SendTicketGrantingTicketAction 發送tgt createTicketGrantingTicketAction-CreateTicketGrantingTicketAction 創建tgt
- DefaultLoginWebflowConfigurer
此類做了一些初始化操作:初始化流,異常,視圖狀態等
protected void doInitialize() {
final Flow flow = getLoginFlow();
if (flow != null) {
createInitialFlowActions(flow);
createDefaultGlobalExceptionHandlers(flow);
createDefaultEndStates(flow);
createDefaultDecisionStates(flow);
createDefaultActionStates(flow);
createDefaultViewStates(flow);
createRememberMeAuthnWebflowConfig(flow);
setStartState(flow, CasWebflowConstants.STATE_ID_INITIAL_AUTHN_REQUEST_VALIDATION_CHECK);
}
-
登錄流程定義
cas登錄流程的完整定義在:web-flow.xml中
<?xml version="1.0" encoding="UTF-8"?>
<flow xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/webflow"
xsi:schemaLocation="http://www.springframework.org/schema/webflow
http://www.springframework.org/schema/webflow/spring-webflow.xsd">
<action-state id="initializeLoginForm">
<evaluate expression="initializeLoginAction" />
<transition on="success" to="viewLoginForm"/>
</action-state>
<view-state id="viewLoginForm" view="casLoginView" model="credential">
<binder>
<binding property="username" required="true"/>
<binding property="password" required="true"/>
</binder>
<transition on="submit" bind="true" validate="true" to="realSubmit" history="invalidate"/>
</view-state>
<action-state id="realSubmit">
<evaluate expression="authenticationViaFormAction"/>
<transition on="warn" to="warn"/>
<transition on="success" to="createTicketGrantingTicket"/>
<transition on="successWithWarnings" to="showAuthenticationWarningMessages"/>
<transition on="authenticationFailure" to="handleAuthenticationFailure"/>
<transition on="error" to="initializeLoginForm"/>
</action-state>
</fl
大致意思就是:
action-state id="initializeLoginForm">
當接收到請求的時候就會初始化登錄表單,由initializeLoginAction處理,如果成功的話,就進入viewLoginForm視圖狀態。
<view-state id="viewLoginForm" view="casLoginView" model="credential">
他綁定了一個credential對象,username,password必填,在頁面點擊提交的時候,觸發realSubmit動作。
<action-state id="realSubmit">
realSubmit狀態由authenticationViaFormAction處理,成功的話,就觸發createTicketGrantingTicket 狀態。如果處理錯誤就回到initializeLoginForm初始化登錄請求。
cas5.3版本中,以上流程較爲簡單,很多細節過程都沒有體現出來,在跟蹤代碼的時候,實際有很多處理邏輯,而在更早版本,web-flow.xml 每個流轉過程都是寫的十分詳細的,大家可以參看:https://my.oschina.net/indestiny/blog/202454,此文對整個流程定義做了詳細的闡述,可以幫助大家理解整個登錄流程。
2. 訪問應用
當我們登錄cas服務端或者通過第三方應用跳轉到cas 服務器時:http://127.0.0.1:8444/cas/login,會經過一系列webflow 流程。典型流程如下:
-
初始化訪問
-
InitialFlowSetupAction
客戶端的請求進來首先會經過InitialFlowSetupAction,doExecute要做的就是把ticketGrantingTicketId,warnCookieValue和service放到FlowScope的作用域中,以便在登錄流程中的state中進行判斷。
public Event doExecute(final RequestContext context) {
final HttpServletRequest request = WebUtils.getHttpServletRequestFromExternalWebflowContext(context);
if (request.getMethod().equalsIgnoreCase(HttpMethod.POST.name())) {
WebUtils.putInitialHttpRequestPostParameters(context);
}
//設置 TGC cookie路徑 /cas 或者/
configureCookieGenerators(context);
//cookie,靜態認證,密碼策略等放入flowScop,TGT放入FlowScope/RequestScope
configureWebflowContext(context);
//將service進行註冊並且放入RequestScope
configureWebflowContextForService(context);
return success();
}
-
InitialAuthenticationRequestValidationAction
初始化認證請求校驗
啥沒幹,就返回一個success的事件,暫時不清楚有什麼具體作用。
protected Event doExecute(final RequestContext requestContext) {
return this.rankedAuthenticationProviderWebflowEventResolver.resolveSingle(requestContext);
}
-
TicketGrantingTicketCheckAction
校驗request 上下文的TGT是否合法。
當我們第一次訪問集成了CAS單點登錄的應用系統,此時應用系統會跳轉到CAS單點登錄的服務器端。此時,request的cookies中不存在CASTGC(TGT),因此FlowScope作用域中的ticketGrantingTicketId爲null
public Event doExecute(final RequestContext requestContext) {
// 第一次tgt爲null 返回tgt不存在的事件
final String tgtId = WebUtils.getTicketGrantingTicketId(requestContext);
if (StringUtils.isBlank(tgtId)) {
return new Event(this, CasWebflowConstants.TRANSITION_ID_TGT_NOT_EXISTS);
}
// 否則就校驗tgt的合法性返回相應的事件
final Ticket ticket = this.centralAuthenticationService.getTicket(tgtId, Ticket.class);
if (ticket != null && !ticket.isExpired()) {
return new Event(this, CasWebflowConstants.TRANSITION_ID_TGT_VALID);
}
return new Event(this, CasWebflowConstants.TRANSITION_ID_TGT_INVALID);
}
-
ServiceAuthorizationCheck
判斷FlowScope作用域中是否存在service,不存在返回成功事件,如果service存在,查找service的註冊信息,判斷service是否符合註冊服務訪問要求,不合法則將未授權的serice放入FlowScope
protected Event doExecute(final RequestContext context) {
final Service service = authenticationRequestServiceSelectionStrategies.resolveService(serviceInContext);
if (service == null) {
return success();
}
final RegisteredService registeredService = this.servicesManager.findServiceBy(service);
if (!registeredService.getAccessStrategy().isServiceAccessAllowed()) {
WebUtils.putUnauthorizedRedirectUrlIntoFlowScope(context, registeredService.getAccessStrategy().getUnauthorizedRedirectUrl());
throw new UnauthorizedServiceException(UnauthorizedServiceException.CODE_UNAUTHZ_SERVICE, msg);
}
return success();
}
-
InitializeLoginAction
初始化登錄行爲,直接返回成功事件,進入登錄頁面
protected Event doExecute(final RequestContext requestContext) throws Exception {
LOGGER.debug("Initialized login sequence");
return success();
}
經過上面一系列的初始化判定,由於第一次訪問,沒有tgt信息,就會顯示出登錄頁面。輸入用戶密碼就會進入下一個階段。
2. 表單提交
-
InitialAuthenticationAction-AbstractAuthenticationAction
此類是處理登陸請求的核心所在,調用鏈真的有點長。整體的步驟就是下面的三個邏輯,複雜的登陸請求代理給initialAuthenticationAttemptWebflowEventResolver處理。
protected Event doExecute(final RequestContext requestContext) {
// 第一次 ticket請求事件處理, 爲null
final Event serviceTicketEvent = this.serviceTicketRequestWebflowEventResolver.resolveSingle(requestContext);
if (serviceTicketEvent != null) {
fireEventHooks(serviceTicketEvent, requestContext);
return serviceTicketEvent;
}
// 此處是調用認證,得到認證的最後結果時間success
final Event finalEvent = this.initialAuthenticationAttemptWebflowEventResolver.resolveSingle(requestContext);
// 觸發認證完成的事件
fireEventHooks(finalEvent, requestContext);
return finalEvent;
}
-------初始認證事件處理器initialAuthenticationAttemptWebflowEventResolver 的resolveSingle幹了啥---------
認證調用鏈如下圖所示:
DefaultAuthenticationSystemSupport:handleInitialAuthenticationTransaction()
DefaultAuthenticationTransactionManager:handle()
PolicyBasedAuthenticationManager:authenticate()
AbstractUsernamePasswordAuthenticationHandler
ChainingPrincipalResolver
以上的委託流程看不懂沒關係,我們挑重要的說:InitialAuthenticationAttemptWebflowEventResolver 這個類主要完成了以下三個事情
-
由PolicyBasedAuthenticationManager處理具體的認證邏輯,返回AuthenticationResultBuilder
-
認證後的事件處理
-
認證結果放入conversationScope
-
獲取AuthenticationResultBuilder
由 InitialAuthenticationAttemptWebflowEventResolver的 resolveInternal方法處理,最後得到AuthenticationResultBuilder
public Set<Event> resolveInternal(final RequestContext context) {
try {
// 從上下文獲取憑證及service
final Credential credential = getCredentialFromContext(context);
final Service service = WebUtils.getService(context);
if (credential != null) {
final AuthenticationResultBuilder builder = this.authenticationSystemSupport.handleInitialAuthenticationTransaction(service, credential);
先看下提交的數據context有些啥?
credential就是提交的用戶和密碼,service則是客戶端的的信息
接下來由PolicyBasedAuthenticationManager 實現類的authenticate方法進行處理:
-
PolicyBasedAuthenticationManager
public Authentication authenticate(final AuthenticationTransaction transaction) throws AuthenticationException {
// 調用認證預處理器
final boolean result = invokeAuthenticationPreProcessors(transaction);
// 將憑證放入當前線程
AuthenticationCredentialsThreadLocalBinder.bindCurrent(transaction.getCredentials());
// 執行內部認證
final AuthenticationBuilder builder = authenticateInternal(transaction);
}
authenticateInternal方法內部認證
這個是核心認證的邏輯
找到認證處理器和用戶解析器,進行認證和解析
// 獲取憑證
final Collection<Credential> credentials = transaction.getCredentials();
//將憑證放入AuthenticationBuilder
final AuthenticationBuilder builder = new DefaultAuthenticationBuilder(NullPrincipal.getInstance());
credentials.forEach(cred -> builder.addCredential(new BasicCredentialMetaData(cred)));
獲取當前事務的認證處理器
final Set<AuthenticationHandler> handlerSet = getAuthenticationHandlersForThisTransaction(transaction);
認證處理器有2個,org.apereo.cas.authentication.AcceptUsersAuthenticationHandler做真正的處理。
下一步,迭代所有的憑證,從handler中找到能夠處理的認證處理器,如果能夠處理,則進行認證及解析用戶信息(Principal)
// 根據handler找到用戶解析器
final PrincipalResolver resolver = getPrincipalResolverLinkedToHandlerIfAny(handler, transaction);
// 認證並解析用戶
authenticateAndResolvePrincipal(builder, credential, resolver, handler);
更多的認證內部實現參見:AbstractUsernamePasswordAuthenticationHandler:doAuthentication()
更多的用戶解析實現參見:ChainingPrincipalResolver:resolve()
認證結果和用戶信息都會放在AuthenticationBuilder認證建造器中,返回builder,內部認證完成下一步對結果進行進一步封裝
- 信息封裝
填充認證方法屬性,認證元數據屬性,調用認證後處理,認證結果放入線程中
final Authentication authentication = builder.build();
addAuthenticationMethodAttribute(builder, authentication);
populateAuthenticationMetadataAttributes(builder, transaction);
invokeAuthenticationPostProcessors(builder, transaction);
final Authentication auth = builder.build();
final Principal principal = auth.getPrincipal();
principal.getId(), principal.getAttributes(), transaction.getCredentials());
//最後將認證結果放入線程中
AuthenticationCredentialsThreadLocalBinder.bindCurrent(auth);
return auth
我們看下最後的認證的結果的builder有些啥:用戶信息,憑證,認證原數據,認證成功的處理器
以上是完成了認證的請求處理
2. 認證調用結束,事件處理
認證完成返回AuthenticationResultBuilder,進行認證後的事件處理,當前流程中並沒有需要處理的事件
final Set<Event> resolvedEvents = resolveCandidateAuthenticationEvents(context, service,
registeredService);
3. 放入緩存
授予TGT給認證結果,其實是將認證結果放入conversationScope,並沒有生成TGT,名字取得有迷惑性
grantTicketGrantingTicketToAuthenticationResult(context, builder, service);
以上的流程纔是真正完成了InitialAuthenticationAction的工作
-
CreateTicketGrantingTicketAction
創建或者更新TGT更新到域中
public Event doExecute(final RequestContext context) {
final Service service = WebUtils.getService(context);
final RegisteredService registeredService = WebUtils.getRegisteredService(context);
// 拿到認證結果建造器
final AuthenticationResultBuilder authenticationResultBuilder = WebUtils.getAuthenticationResultBuilder(context);
//先從請求域或者flow中獲取tgt->null
final String ticketGrantingTicket = WebUtils.getTicketGrantingTicketId(context);
// 基於ticketGrantingTicket生成新的TGT
final TicketGrantingTicket tgt = createOrUpdateTicketGrantingTicket(authenticationResult, authentication, ticketGrantingTicket);
// 緩存TGT
WebUtils.putTicketGrantingTicketInScopes(context, tgt);
WebUtils.putAuthenticationResult(authenticationResult, context);
WebUtils.putAuthentication(tgt.getAuthentication(), context);
return success();
}
-
SendTicketGrantingTicketAction
將TGT放入cookie,以及處理TGT的銷燬,返回成功
final String ticketGrantingTicketId = WebUtils.getTicketGrantingTicketId(context);
final String ticketGrantingTicketValueFromCookie = WebUtils.getTicketGrantingTicketIdFrom(context.getFlowScope());
if (this.renewalStrategy.isParticipating(context)) {
//將TGT放入cookie
this.ticketGrantingTicketCookieGenerator.addCookie(context, ticketGrantingTicketId);
}
if (ticketGrantingTicketValueFromCookie != null && !ticketGrantingTicketId.equals(ticketGrantingTicketValueFromCookie)) {
// 如果二者不匹配,將cookie中的tgt刪除
this.centralAuthenticationService.destroyTicketGrantingTicket(ticketGrantingTicketValueFromCookie);
-
GenerateServiceTicketAction
完成認證,對指定的TGT和service生成唯一的st,st只是作爲客戶端的認證標識,而且只有在訪問/login的時候會生成
根據TGT獲取到ticket ,裏面包含了認證結果,獲取到認證信息
//獲取service及TGT
final Service service = WebUtils.getService(context);
final String ticketGrantingTicket = WebUtils.getTicketGrantingTicketId(context);
try {
// 根據TGT獲取到認證信息
final Authentication authentication = this.ticketRegistrySupport.getAuthenticationFrom(ticketGrantingTicket);
final Service selectedService = authenticationRequestServiceSelectionStrategies.resolveService(service);
final RegisteredService registeredService = servicesManager.findServiceBy(selectedService);
WebUtils.putRegisteredService(context, registeredService);
WebUtils.putService(context, service);
if (registeredService != null) {
final URI url = registeredService.getAccessStrategy().getUnauthorizedRedirectUrl();
// 將service中的未授權的url緩存起來
WebUtils.putUnauthorizedRedirectUrlIntoFlowScope(context, url);
}
final Credential credential = WebUtils.getCredential(context);
// 根據憑證和認證信息構建一個認證結果建造器builder
final AuthenticationResultBuilder builder = this.authenticationSystemSupport.establishAuthenticationContextFromInitial(authentication, credential);
final AuthenticationResult authenticationResult = builder.build(principalElectionStrategy, service);
// 生成st
final ServiceTicket serviceTicketId = this.centralAuthenticationService.grantServiceTicket(ticketGrantingTicket, service, authenticationResult);
//放入RequestScope
WebUtils.putServiceTicketInRequestScope(context, serviceTicketId);
return success()
生成st的邏輯詳見:DefaultCentralAuthenticationService:grantServiceTicket()
TGT和ST的關係:一對多,因爲一個tgt代表一個用戶會話,而一個用戶可以訪問多個應用,每個應用都會生成一個應用會話,即ServiceTicket
tgtId:TGT-4-ZI-xw7c3rqX-zF-vzYj1Hegm4gg3LQRip8BvKYcIoe0jT3rd-pDfTKnBKnPWFLsqcBAA013935-PC
stId:ST-4-8E5BJFvGOc57nvvTh8jebaYzOBYA013935-PC
AuthenticationResult和 Authentication的關係,前者多了credentialProvided和service屬性
-
RedirectToServiceAction
將生成的st和service拼接,重定向到客戶端
protected Event doExecute(final RequestContext requestContext) {
//http://portal.demo.qds.sd:18835/
final WebApplicationService service = WebUtils.getService(requestContext);
final Authentication auth = WebUtils.getAuthentication(requestContext);
// ST-4-8E5BJFvGOc57nvvTh8jebaYzOBYA013935-PC
final String serviceTicketId = WebUtils.getServiceTicketFromRequestScope(requestContext);
final ResponseBuilder builder = responseBuilderLocator.locate(service);
// http://portal.demo.qds.sd:18835/?ticket=ST-4-8E5BJFvGOc57nvvTh8jebaYzOBYA013935-PC
final Response response = builder.build(service, serviceTicketId, auth);
return finalizeResponseEvent(requestContext, service, response);
}
從response中可以看到,下一步將重定向到service中,並在url後跟上st
4. 驗證ST
注意st有過期時間限制,斷點的時候就會過期
/cas/p3/validateService
進入controller
org.apereo.cas.web.v3.V3ServiceValidateController:handle(),返回一個視圖
@GetMapping(path = CasProtocolConstants.ENDPOINT_SERVICE_VALIDATE_V3)
protected ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response) throws Exception {
return super.handleRequestInternal(request, response);
}
調用鏈:
下面看下主要validateServiceTicket方法的主要邏輯:
-
獲取service
從st獲取service和當前request請求中的service
//根據st獲取到ServiceTicket對象
final ServiceTicket serviceTicket = this.ticketRegistry.getTicket(serviceTicketId, ServiceTicket.class);
// 從st獲取service和當前請求中的service
final Service selectedService = resolveServiceFromAuthenticationRequest(serviceTicket.getService());
final Service resolvedService = resolveServiceFromAuthenticationRequest(service);
2. 校驗
判斷service是否過期,二者是否相等,校驗不過拋出異常
if (serviceTicket.isExpired())
if (!serviceTicket.isValidFor(resolvedService))
3. 獲取信息
根據st獲取到TGT,裏面包含認證信息authentication和service,認證信息包含用戶信息Principal
final TicketGrantingTicket root = serviceTicket.getTicketGrantingTicket().getRoot();
final Authentication authentication = getAuthenticationSatisfiedByPolicy(root.getAuthentication(),
new ServiceContext(selectedService, registeredService));
final Principal principal = authentication.getPrincipal();
4. 構建Assertion
根據認證信息構建Assertion對象,最後更新st
final Assertion assertion = new DefaultAssertionBuilder(finalAuthentication)
.with(selectedService)
.with(serviceTicket.getTicketGrantingTicket().getChainedAuthentications())
.with(serviceTicket.isFromNewLogin())
.build();
//最後更新st
this.ticketRegistry.updateTicket(serviceTicket);
Assertion對象都有什麼:
看下最後客戶端收到的信息是什麼:
5. 登出
當訪問/cas/logout時候,會進入TerminateSessionAction中
-
TerminateSessionAction
@Override
public Event doExecute(final RequestContext requestContext) {
boolean terminateSession = true;
// isConfirmLogout = false
if (logoutProperties.isConfirmLogout()) {
terminateSession = isLogoutRequestConfirmed(requestContext);
}
if (terminateSession) {
// 下一步去銷燬session
return terminate(requestContext);
}
return this.eventFactorySupport.event(this, CasWebflowConstants.STATE_ID_WARN);
}
context中到底有些啥信息
此action的主要邏輯爲:
- 獲取request和response;
- 從域中獲取tgtId,結果爲null;
- 從cookie中獲取tgtId,實際通過從request,header中獲取
- 通知tgt所關聯的應用下線
- 銷燬cookie,銷燬session
public Event terminate(final RequestContext context) {
final HttpServletRequest request = WebUtils.getHttpServletRequestFromExternalWebflowContext(context);
final HttpServletResponse response = WebUtils.getHttpServletResponseFromExternalWebflowContext(context);
//null
String tgtId = WebUtils.getTicketGrantingTicketId(context);
if (StringUtils.isBlank(tgtId)) {
//從cookie中獲取
tgtId = this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request);
}
if (StringUtils.isNotBlank(tgtId)) {
LOGGER.debug("Destroying SSO session linked to ticket-granting ticket [{}]", tgtId);
// 通知tgt所關聯的應用下線
final List<LogoutRequest> logoutRequests = this.centralAuthenticationService.destroyTicketGrantingTicket(tgtId);
WebUtils.putLogoutRequests(context, logoutRequests);
}
// 銷燬cookie
LOGGER.debug("Removing CAS cookies");
this.ticketGrantingTicketCookieGenerator.removeCookie(response);
this.warnCookieGenerator.removeCookie(response);
// 銷燬session
destroyApplicationSession(request, response);
LOGGER.debug("Terminated all CAS sessions successfully.");
if (StringUtils.isNotBlank(logoutProperties.getRedirectUrl())) {
WebUtils.putLogoutRedirectUrl(context, logoutProperties.getRedirectUrl());
return this.eventFactorySupport.event(this, CasWebflowConstants.STATE_ID_REDIRECT);
}
return this.eventFactorySupport.success(this);
通知應用下線:
- 獲取TicketGrantingTicket對象
- 由登出管理者logoutManager處理登出
- 返回登出結果
public List<LogoutRequest> destroyTicketGrantingTicket(final String ticketGrantingTicketId) {
try {
// 根據ticketGrantingTicketId 獲取 TicketGrantingTicket對象
final TicketGrantingTicket ticket = getTicket(ticketGrantingTicketId, TicketGrantingTicket.class);
// 將ticket關聯的認證信息放入線程 AuthenticationCredentialsThreadLocalBinder.bindCurrent(ticket.getAuthentication());
final List<LogoutRequest> logoutRequests = this.logoutManager.performLogout(ticket);
// 刪除ticket
deleteTicket(ticketGrantingTicketId);
//事件發佈
doPublishEvent(new CasTicketGrantingTicketDestroyedEvent(this, ticket));
return logoutRequests;
} catch (final InvalidTicketException e) {
LOGGER.debug("TicketGrantingTicket [{}] cannot be found in the ticket registry.", ticketGrantingTicketId);
}
return new ArrayList<>(0);
此處處理鏈較長:從TerminateSessionAction: doExecute()方法開始一直到DefaultSingleLogoutServiceMessageHandler:performBackChannelLogout()方法
直接看最後的處理邏輯:
通過httpClient發送請求
public boolean performBackChannelLogout(final LogoutRequest request) {
try {
LOGGER.debug("Creating back-channel logout request based on [{}]", request);
final String logoutRequest = this.logoutMessageBuilder.create(request);
final WebApplicationService logoutService = request.getService();
logoutService.setLoggedOutAlready(true);
LOGGER.debug("Preparing logout request for [{}] to [{}]", logoutService.getId(), request.getLogoutUrl());
final LogoutHttpMessage msg = new LogoutHttpMessage(request.getLogoutUrl(), logoutRequest, this.asynchronous);
LOGGER.debug("Prepared logout message to send is [{}]. Sending...", msg);
return this.httpClient.sendMessageToEndPoint(msg);
} catch (final Exception e) {
LOGGER.error(e.getMessage(), e);
}
return false;
}
簡單看下發送的消息內容:post 表單請求,攜帶logoutRequest請求參數,其值爲xml格式的字符串,包含了 st
登出結果LogoutRequest
參考文檔:
https://my.oschina.net/indestiny/blog/202454
https://blog.csdn.net/dovejing/article/details/44523545