• 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-xutingcas-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-xutingcas-xuting.keystore" -file "E:cas-xutingCASCHENJIE.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> 





  • 相关阅读:
    第十一周课程总结
    第十周课程总结
    第九周课程总结&实验报告
    第八周课程总结&实验报告
    第七周课程总结&实验报告
    第六周课程总结&实验报告
    课程总结
    第十四周
    第十三周
    第十二周
  • 原文地址:https://www.cnblogs.com/ttmemory/p/7908109.html
Copyright © 2020-2023  润新知