CAS單點登錄(一):服務端搭建


 

1.下載

先在網上下載cas-server-3.5.2,將里面的cas-server-webapp-3.5.2.war放到tomcat的webapps目錄下。

2.https設置

cas單點登默認使用的是https,所以需要證書,由於是個人測試和學習用的,可以用JDK自帶的keytool工具生成證書。

2.1用JDK生成證書:

 

keytool -genkey -keystore "E:\cas-xuting\cas-xuting.keystore" -alias "CASCHENJIE" -keyalg "RSA" -validity 36500 -dname "CN=localhost,OU=org.cj,L=cd,ST=sc,C=c" -keypass "xuting" -storepass "
xuting"

 

 2.2頒發證書

 

keytool -alias "CASCHENJIE" -exportcert -keystore "E:\cas-xuting\cas-xuting.keystore" -file "E:\cas-xuting\CASCHENJIE.cer" -storepass "xuting"

2.3安裝證書

雙擊CASCHEBJIE.cer安裝證書
或將證書存儲到受信任的根證書頒發機構。
或者為客戶端的JVM導入證書:

2.4配置tomcat服務器

<Connector port="8443" protocol="org.apache.coyote.http11.Http11Protocol"
               maxThreads="150" SSLEnabled="true" scheme="https" secure="true"
               clientAuth="false" sslProtocol="TLS"
                keystoreFile="E:/cas-xuting/cas-xuting.keystore" keystorePass="xuting"/>

 

3.啟動tomcat測試

 當秘密和用戶名相同時登錄成功

這樣一個簡單的CAS的單點登錄的服務端就搭建成功。

4.CAS服務端的源碼解析

更具點單登錄流程走,流程配置在/WEB-INF/login-webflow.xml文件中。

 4.1設置一個變量credentials來存放用戶名和密碼信息

<var name="credentials" class="org.jasig.cas.authentication.principal.UsernamePasswordCredentials" />  

 

 4.2流程初始化從on-start標簽開始

<on-start>  
    <evaluate expression="initialFlowSetupAction" />  
</on-start> 

 

4.2.1 initialFlowSetupAction的配置在/WEB-INF/cas-servlet.xml中
<bean id="initialFlowSetupAction" class="org.jasig.cas.web.flow.InitialFlowSetupAction"  
        p:argumentExtractors-ref="argumentExtractors"  
        p:warnCookieGenerator-ref="warnCookieGenerator"  
        p:ticketGrantingTicketCookieGenerator-ref="ticketGrantingTicketCookieGenerator"/>  

 

4.2.1.1argumentExtractors的配置又在/WEB-INF/spring-configuration/argumentExtractorsConfiguration.xml中。
<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>

4.2.1.2其中warnCookieGenerator配置文件在/WEB-INF/spring-configuration/warnCookieGenerator.xml中。

<bean id="warnCookieGenerator" class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator"  
    p:cookieSecure="true"  
    p:cookieMaxAge="-1"  
    p:cookieName="CASPRIVACY"  
    p:cookiePath="/cas" />  

4.2.1.3其中ticketGrantingTicketCookieGenerator配置文件在/WEB-INF/spring-configuration/ticketGrantingTicketCookieGenerator.xml中。

<bean id="ticketGrantingTicketCookieGenerator" class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator"  
    p:cookieSecure="false"  
    p:cookieMaxAge="-1"  
    p:cookieName="CASTGC"  
    p:cookiePath="/cas" />  

 

4.2.2初始化部分會調用InitialFlowSetupAction的doExecute方法,如果有特殊需求,可以在此方法中增加相應的邏輯。如果希望單點登錄集成統一身份認證,那么可以在此處增加統一身份認證的邏輯。
InitialFlowSetupAction的doExecute方法:
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;  
    }  
    //將TGT放在FlowScope作用域中  
    context.getFlowScope().put(  
        "ticketGrantingTicketId", this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request));  
    //將warnCookieValue放在FlowScope作用域中  
    context.getFlowScope().put(  
        "warnCookieValue", Boolean.valueOf(this.warnCookieGenerator.retrieveCookieValue(request)));  
    //獲取service參數  
    final Service service = WebUtils.getService(this.argumentExtractors, context);  
  
    if (service != null && logger.isDebugEnabled()) {  
        logger.debug("Placing service in FlowScope: " + service.getId());  
    }  
    //將service放在FlowScope作用域中  
    context.getFlowScope().put("service", service);  
  
    return result("success");  
}  

4.3InitialFlowSetupAction的doExecute要做的就是把ticketGrantingTicketId,warnCookieValue和service放到FlowScope的作用域中,以便在登錄流程中的state中進行判斷。初始化完成后,登錄流程流轉到第一個state(ticketGrantingTicketExistsCheck)。

<decision-state id="ticketGrantingTicketExistsCheck">  
    <if test="flowScope.ticketGrantingTicketId != null" then="hasServiceCheck" else="gatewayRequestCheck" />  
</decision-state> 

4.4當我們第一次訪問集成了CAS單點登錄的應用系統(假設為:webapp1)時(http://127.0.0.1:8090/webapp1/main.do),此時應用系統會跳轉到CAS單點登錄的服務器端(http://127.0.0.1:8081/cas-server/login?service=http://127.0.0.1:8090/webapp1/main.do)。此時,request的cookies中不存在CASTGC(TGT),因此FlowScope作用域中的ticketGrantingTicketId為null,登錄流程流轉到第二個state(gatewayRequestCheck)。

<decision-state id="gatewayRequestCheck">  
    <if test="requestParameters.gateway != '' and requestParameters.gateway != null and flowScope.service != null"   
        then="gatewayServicesManagementCheck" else="serviceAuthorizationCheck" />  
</decision-state> 

 

 
        
4.5因為初始化時,盡管把service保存在了FlowScope作用域中,但request中的參數gateway不存在,登錄流程流轉到第三個state(serviceAuthorizationCheck)。
<action-state id="serviceAuthorizationCheck">  
    <evaluate expression="serviceAuthorizationCheck"/>  
    <transition to="generateLoginTicket"/>  
</action-state>  

 

4.5.1ServiceAuthorizationCheck的doExecute方法
protected Event doExecute(final RequestContext context) throws Exception {  
    final Service service = WebUtils.getService(context);  
    //No service == plain /login request. Return success indicating transition to the login form  
    if(service == null) {  
        return success();  
    }  
    final RegisteredService registeredService = this.servicesManager.findServiceBy(service);  
  
    if (registeredService == null) {  
        logger.warn("Unauthorized Service Access for Service: [ {} ] - service is not defined in the service registry.", service.getId());  
        throw new UnauthorizedServiceException();  
    }  
    else if (!registeredService.isEnabled()) {  
        logger.warn("Unauthorized Service Access for Service: [ {} ] - service is not enabled in the service registry.", service.getId());  
        throw new UnauthorizedServiceException();  
    }  
  
    return success();  
}  

 

4.6ServiceAuthorizationCheck的doExecute方法,要做的就是判斷FlowScope作用域中是否存在service,如果service存在,查找service的注冊信息。登錄流程流轉到第四個state(generateLoginTicket)。
<action-state id="generateLoginTicket">  
    <evaluate expression="generateLoginTicketAction.generate(flowRequestContext)" />  
    <transition on="generated" to="viewLoginForm" />  
</action-state>  

 

4.6.1generateLoginTicketAction配置在/WEB-INF/cas-servlet.xml中。

<bean id="generateLoginTicketAction" class="org.jasig.cas.web.flow.GenerateLoginTicketAction"  
    p:ticketIdGenerator-ref="loginTicketUniqueIdGenerator" />  

 

4.6.1.1其中loginTicketUniqueIdGenerator又在/WEB-INF/spring-configuration/uniqueIdGenerators.xml中

<bean id="loginTicketUniqueIdGenerator" class="org.jasig.cas.util.DefaultUniqueTicketIdGenerator">  
    <constructor-arg  
        index="0"  
        type="int"  
        value="30" />  
</bean>  

 

4.6.1.2DefaultUniqueTicketIdGenerator要做的就是生成以LT作為前綴的loginTicket(例:LT-2-pfDmbEHfX2OkS0swLtDd7iDwmzlhsn)。注:LT只作為登錄時使用的票據

GenerateLoginTicketAction的generate方法

public final String generate(final RequestContext context) {  
    //LT-2-pfDmbEHfX2OkS0swLtDd7iDwmzlhsn  
    final String loginTicket = this.ticketIdGenerator.getNewTicketId(PREFIX);//生成loginTicket  
    this.logger.debug("Generated login ticket " + loginTicket);  
    WebUtils.putLoginTicket(context, loginTicket);//放到flowScope中  
    return "generated";  
} 

 

4.7GenerateLoginTicketAction的generate要做的就是生成loginTicket,並且把loginTicket放到FlowScope作用域中。登錄流程流轉到第五個state(viewLoginForm)。

<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 expression="authenticationViaFormAction.doBind(flowRequestContext, flowScope.credentials)" />  
    </transition>  
</view-state>  

至此,經過五個state的流轉,我們完成了第一次訪問集成了單點登錄的應用系統,此時流轉到CAS單點登錄服務器端的登錄頁面/WEB-INF/jsp/ui/default/casLoginView.jsp。由於casLoginView.jsp是CAS提供的默認登錄頁面,需要把此頁面修改成我們系統需要的登錄頁面,格式需要參考casLoginView.jsp。

注意,默認的登錄頁面中有lt、execution和_eventId三個隱藏參數,lt參數值就是在GenerateLoginTicketAction的generate方法中生成的loginTicket。

<input type="hidden" name="lt" value="${loginTicket}" />  
<input type="hidden" name="execution" value="${flowExecutionKey}" />  
<input type="hidden" name="_eventId" value="submit" /> 
5CAS單點登錄服務器端的登錄驗證
5.1當輸入用戶名和密碼,點擊登錄按鈕時,會執行AuthenticationViaFormAction的doBind方法。(
上文4.7中authenticationViaFormAction可以知道)
它的的配置在/WEB-INF/cas-servlet.xml中
 <bean id="authenticationViaFormAction" class="org.jasig.cas.web.flow.AuthenticationViaFormAction"
        p:centralAuthenticationService-ref="centralAuthenticationService"
        p:warnCookieGenerator-ref="warnCookieGenerator"/>

 

 AuthenticationViaFormAction的doBind方法
public final void doBind(final RequestContext context, final Credentials credentials) throws Exception {  
    final HttpServletRequest request = WebUtils.getHttpServletRequest(context);  
    //bean中沒有注入,這里什么也不做  
    if (this.credentialsBinder != null && this.credentialsBinder.supports(credentials.getClass())) {  
        this.credentialsBinder.bind(request, credentials);  
    }  
}  

 


5.1.1登錄流程流轉到第一個state(realSubmit),會執行AuthenticationViaFormAction的submit方法 
<action-state id="realSubmit">  
    <evaluate expression="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方法
public final String submit(final RequestContext context, final Credentials credentials, final MessageContext messageContext)   
    throws Exception {  
    // Validate login ticket  
    final String authoritativeLoginTicket = WebUtils.getLoginTicketFromFlowScope(context);  
    final String providedLoginTicket = WebUtils.getLoginTicketFromRequest(context);  
    //判斷FlowScope和request中的loginTicket是否相同  
    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";  
    }  
    //requestScope和FlowScope中獲取TGT  
    final String ticketGrantingTicketId = WebUtils.getTicketGrantingTicketId(context);  
    //FlowScope中獲取service  
    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);  
            if (logger.isDebugEnabled()) {  
                logger.debug("Attempted to generate a ServiceTicket using renew=true with different credentials", e);  
            }  
        }  
    }  
  
    try {  
        //根據用戶憑證構造TGT,把TGT放到requestScope中,同時把TGT緩存到服務器的cache<ticketId,TGT>中  
        WebUtils.putTicketGrantingTicketInRequestScope(context,   
            this.centralAuthenticationService.createTicketGrantingTicket(credentials));  
        putWarnCookieIfRequestParameterPresent(context);  
        return "success";  
    } catch (final TicketException e) {  
        populateErrorsInstance(e, messageContext);  
        if (isCauseAuthenticationException(e))  
            return getAuthenticationExceptionEventId(e);  
        return "error";  
    }  
}  

 

5.2AuthenticationViaFormAction的submit要做的就是判斷FlowScope和request中的loginTicket是否相同。如果不同跳轉到錯誤頁面,如果相同,則根據用戶憑證生成TGT(登錄成功票據),並放到requestScope作用域中,同時把TGT緩存到服務器的cache<ticketId,TGT>中。登錄流程流轉到第二個state(sendTicketGrantingTicket)。

既然是登錄,那么可以在此方法中加入自己的業務邏輯,比如,可以加入驗證碼的判斷,以及錯誤信息的提示,用戶名或者密碼錯誤,驗證碼錯誤等邏輯判斷。

<action-state id="sendTicketGrantingTicket">  
    <evaluate expression="sendTicketGrantingTicketAction" />  
    <transition to="serviceCheck" />  
</action-state>  

 

5.2.1SendTicketGrantingTicketAction的doExecute方法

protected Event doExecute(final RequestContext context) {  
    //requestScope和FlowScope中獲取TGT  
    final String ticketGrantingTicketId = WebUtils.getTicketGrantingTicketId(context);   
    final String ticketGrantingTicketValueFromCookie = (String) context.getFlowScope().get("ticketGrantingTicketId");  
          
    if (ticketGrantingTicketId == null) {  
        return success();  
    }  
    //response中添加TGC  
    this.ticketGrantingTicketCookieGenerator.addCookie(WebUtils.getHttpServletRequest(context), WebUtils  
        .getHttpServletResponse(context), ticketGrantingTicketId);  
  
    if (ticketGrantingTicketValueFromCookie != null && !ticketGrantingTicketId.equals(ticketGrantingTicketValueFromCookie)) {  
        this.centralAuthenticationService  
            .destroyTicketGrantingTicket(ticketGrantingTicketValueFromCookie);  
    }  
  
    return success();  
}  

 

5.3SendTicketGrantingTicketAction的doExecute要做的是獲取TGT,並根據TGT生成cookie添加到response。登錄流程流轉到第三個state(serviceCheck)。

<decision-state id="serviceCheck">  
    <if test="flowScope.service != null" then="generateServiceTicket" else="viewGenericLoginSuccess" />  
</decision-state>  
5.4由於此時FlowScope中存在service(http://127.0.0.1:8081/cas-server/login?service=http://127.0.0.1:8090/webapp1/main.do),登錄流程流轉到第四個state(generateServiceTicket)。
<action-state id="generateServiceTicket">  
    <evaluate expression="generateServiceTicketAction" />  
    <transition on="success" to ="warn" />  
    <transition on="error" to="generateLoginTicket" />  
    <transition on="gateway" to="gatewayServicesManagementCheck" />  
</action-state>  

 

5.4.1其中GenerateServiceTicketAction的doExecute方法
protected Event doExecute(final RequestContext context) {  
    //獲取service  
    final Service service = WebUtils.getService(context);  
    //獲取TGT  
    final String ticketGrantingTicket = WebUtils.getTicketGrantingTicketId(context);  
  
    try {  
        //根據TGT和service生成service ticket(ST-2-97kwhcdrBW97ynpBbZH5-cas01.example.org)  
        final String serviceTicketId = this.centralAuthenticationService.grantServiceTicket(ticketGrantingTicket,  
            service);  
        //ST放到requestScope中  
        WebUtils.putServiceTicketInRequestScope(context, serviceTicketId);  
        return success();  
    } catch (final TicketException e) {  
        if (isGatewayPresent(context)) {  
            return result("gateway");  
        }  
    }  
  
    return error();  
}  

 

5.5GenerateServiceTicketAction的doExecute要做的是獲取service和TGT,並根據service和TGT生成以ST為前綴的serviceTicket(例:ST-2-97kwhcdrBW97ynpBbZH5-cas01.example.org),並把serviceTicket放到requestScope中。登錄流程流轉到第五個state(warn)。
<decision-state id="warn">  
    <if test="flowScope.warnCookieValue" then="showWarningView" else="redirect" />  
</decision-state>  

 

5.6由於此時FlowScope中不存在warnCookieValue,登錄流程流轉到第六個state(redirect)。
<action-state id="redirect">  
    <evaluate expression="flowScope.service.getResponse(requestScope.serviceTicketId)"   
        result-type="org.jasig.cas.authentication.principal.Response" result="requestScope.response" />  
    <transition to="postRedirectDecision" />  
</action-state>  

5.7從requestScope中獲取serviceTicket,構造response對象,並把response放到requestScope中。登錄流程流轉到第七個state(postRedirectDecision)。

<decision-state id="postRedirectDecision">  
    <if test="requestScope.response.responseType.name() == 'POST'" then="postView" else="redirectView" />  
</decision-state>  

 

5.8由於request請求(http://127.0.0.1:8081/cas-server/login?service=http://127.0.0.1:8090/webapp1/main.do)是get類型,登錄流程流轉到第八個state(redirectView)
<end-state id="redirectView" view="externalRedirect:${requestScope.response.url}" />  

此時流程如下:

  1. 跳轉到應用系統(http://127.0.0.1:8090/webapp1/main.do?ticket=ST-1-4hH2s5tzsMGCcToDvGCb-cas01.example.org)。
  2. 進入CAS客戶端的AuthenticationFilter過濾器,由於session中獲取名為“_const_cas_assertion_”的assertion對象不存在,但是request有ticket參數,所以進入到下一個過濾器。
  3. TicketValidationFilter過濾器的validate方法通過httpClient訪問CAS服務器端(http://127.0.0.1:8081/cas-server/serviceValidate?ticket=ST-1-4hH2s5tzsMGCcToDvGCb-cas01.example.org&service=http://127.0.0.1:8090/webapp1/main.do)驗證ticket是否正確,並返回assertion對象。
5.9Assertion對象格式類似於
<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>  
    <cas:authenticationSuccess>  
        <cas:user>system</cas:user>  
  
    </cas:authenticationSuccess>  
</cas:serviceResponse> 

 






 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM