SSO单点登录------源码分析

4 源码解析

4.1 Server源码解析

Cas server端采用Spring WebFlow来进行流程控制,因此本文以系统webflow文件为切入点,对流程相关源码进行分析。Cas系统的webflow文件位于WEB-INF/webflow目录下,分为登陆流程和登出流程。

4.1.1 登陆流程解析

4.1.1.1 访问接入Cas系统的应用系统Client1

登陆流程配置文件为login-webflow.xm。
浏览器首次访问配置了单点登录的应用系统时(http://www.client1.com/index),Client1会将请求重定向到cas系统
(http://www.casserver.com/serviceValidate?service=http://www.client1.com/index)
cas系统接收到浏览器发来的请求,整个登录流程从此处开始,流程初始化。
WEB-INF/login-webflow.xml部分代码:

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

初始化部分会调用InitialFlowSetupAction类的doExecute方法,如果有特殊需求,可以在此方法中增加相应的逻辑。
InitialFlowSetupAction的doExecute方法:

@Override
    protected Event doExecute(final RequestContext context) throws Exception {
        final HttpServletRequest request = WebUtils.getHttpServletRequest(context);

        final String contextPath = context.getExternalContext().getContextPath();
        final String cookiePath = StringUtils.isNotBlank(contextPath) ? contextPath + '/' : "/";

        if (StringUtils.isBlank(warnCookieGenerator.getCookiePath())) {
            logger.info("Setting path for cookies for warn cookie generator to: {} ", cookiePath);
            this.warnCookieGenerator.setCookiePath(cookiePath);
        } else {
            logger.debug("Warning cookie path is set to {} and path {}", warnCookieGenerator.getCookieDomain(),
                    warnCookieGenerator.getCookiePath());
        }
        if (StringUtils.isBlank(ticketGrantingTicketCookieGenerator.getCookiePath())) {
            logger.info("Setting path for cookies for TGC cookie generator to: {} ", cookiePath);
            this.ticketGrantingTicketCookieGenerator.setCookiePath(cookiePath);
        } else {
            logger.debug("TGC cookie path is set to {} and path {}", ticketGrantingTicketCookieGenerator.getCookieDomain(),
                    ticketGrantingTicketCookieGenerator.getCookiePath());
        }
//将TGT放在FlowScope作用域中 
        WebUtils.putTicketGrantingTicketInScopes(context,
                this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request));
//将warnCookieValue放在FlowScope作用域中
        WebUtils.putWarningCookie(context,
                Boolean.valueOf(this.warnCookieGenerator.retrieveCookieValue(request)));
//获取service参数
        final Service service = WebUtils.getService(this.argumentExtractors, context);


        if (service != null) {
            logger.debug("Placing service in context scope: [{}]", service.getId());

            final RegisteredService registeredService = this.servicesManager.findServiceBy(service);
            if (registeredService != null && registeredService.getAccessStrategy().isServiceAccessAllowed()) {
                logger.debug("Placing registered service [{}] with id [{}] in context scope",
                        registeredService.getServiceId(),
                        registeredService.getId());
                WebUtils.putRegisteredService(context, registeredService);

                final RegisteredServiceAccessStrategy accessStrategy = registeredService.getAccessStrategy();
                if (accessStrategy.getUnauthorizedRedirectUrl() != null) {
                    logger.debug("Placing registered service's unauthorized redirect url [{}] with id [{}]in context scope",
                            accessStrategy.getUnauthorizedRedirectUrl(),
                            registeredService.getServiceId());
                    WebUtils.putUnauthorizedRedirectUrl(context, accessStrategy.getUnauthorizedRedirectUrl());
                }
            }
        } else if (!this.enableFlowOnAbsentServiceRequest) {
            logger.warn("No service authentication request is available at [{}]. CAS is configured to disable the flow.",
                    WebUtils.getHttpServletRequest(context).getRequestURL());
            throw new NoSuchFlowExecutionException(context.getFlowExecutionContext().getKey(),
                    new UnauthorizedServiceException("screen.service.required.message", "Service is required"));
        }
//将service放在FlowScope作用域中 
        WebUtils.putService(context, service);
        return result("success");
    }

InitialFlowSetupAction的doExecute要做的就是把ticketGrantingTicketId,warnCookieValue和service放到FlowScope的作用域中,以便在登录流程中的state中进行判断。初始化完成后,登录流程流转到第一个state(ticketGrantingTicketExistsCheck)。

<action-state id="ticketGrantingTicketCheck">
        <evaluate expression="ticketGrantingTicketCheckAction"/>
        <transition on="notExists" to="gatewayRequestCheck"/>
        <transition on="invalid" to="terminateSession"/>
        <transition on="valid" to="hasServiceCheck"/>
    </action-state>

ticketGrantingTicketCheckAction的doExecute方法判断request的Cookie中是否携带有效的TGT,第一次访问时没有携带TGT,流程跳转到gatewayRequestCheck。

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

因为初始化时,尽管把service保存在了FlowScope作用域中,但request中的参数gateway不存在,登录流程流转到第三个state(serviceAuthorizationCheck)。

 <action-state id="serviceAuthorizationCheck">
        <evaluate expression="serviceAuthorizationCheck"/>
        <transition to="initializeLogin"/>
    </action-state>

ServiceAuthorizationCheck的doExecute方法,要做的就是判断FlowScope作用域中是否存在service,如果service存在,查找service的注册信息。登录流程流转到第四个state(generateLoginTicket)。

  <action-state id="initializeLogin">
        <evaluate expression="'success'"/>
        <transition on="success" to="viewLoginForm"/>
    </action-state>

initializeLogin不做判断,存在只是为了兼容旧cas版本。直接跳转到viewLoginForm。

<view-state id="viewLoginForm" view="casLoginView" model="credential">
        <binder>
            <binding property="username" required="true"/>
            <binding property="password" required="true"/>
        </binder>
        <on-entry>
            <set name="viewScope.commandName" value="'credential'"/>
        </on-entry>
        <transition on="submit" bind="true" validate="true" to="realSubmit"/>
    </view-state>

此时流转到CAS单点登录服务器端的登录页面casLoginView.jsp。

<action-state id="realSubmit">
        <evaluate
                expression="authenticationViaFormAction.submit(flowRequestContext, flowScope.credential, messageContext)"/>
        <transition on="warn" to="warn"/>
        <transition on="success" to="sendTicketGrantingTicket"/>
        <transition on="successWithWarnings" to="showMessages"/>
        <transition on="authenticationFailure" to="handleAuthenticationFailure"/>
        <transition on="error" to="initializeLogin"/>
    </action-state>

用户在登录页面输入账号密码提交后,流程走到realSumit。
authenticationViaFormAction类的submit()对用户提交的认证信息进行验证。

public final Event submit(final RequestContext context, final Credential credential,
                              final MessageContext messageContext)  {
        //判断是否是已登录过,请求ST的
        if (isRequestAskingForServiceTicket(context)) {
              //如果已登录,则生成ST
return grantServiceTicket(context, credential);
        }
       //未登陆过,生成TGT
        return createTicketGrantingTicket(context, credential, messageContext);
    }

验证成功则跳转到sendTicketGrantingTicket。

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

接着跳转到serviceCheck

<decision-state id="serviceCheck">
        <if test="flowScope.service != null" then="generateServiceTicket" else="viewGenericLoginSuccess"/>
    </decision-state>

判断是否是由应用页面跳转到登录页面登陆的,如果是,则跳转到generateServiceTicket,不是则跳转到viewGenericLoginSuccess。
此处我们跳转到generateServiceTicket

 <action-state id="generateServiceTicket">
        <evaluate expression="generateServiceTicketAction"/>
        <transition on="success" to="warn"/>
        <transition on="unregisteredService" to="viewGenericLoginSuccess"/>
        <transition on="authenticationFailure" to="handleAuthenticationFailure"/>
        <transition on="error" to="initializeLogin"/>
        <transition on="gateway" to="gatewayServicesManagementCheck"/>
    </action-state>

generateServiceTicketAction类的doExecute方法生成ST,并跳转到warn

 <decision-state id="warn">
        <if test="flowScope.warnCookieValue" then="showWarningView" else="redirect"/>
    </decision-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>

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

    <end-state id="redirectView" view="externalRedirect:#{requestScope.response.url}"/>

最终返回给浏览器跳转回Client1的响应。

4.1.1.2 访问接入Cas系统的应用系统Client2

访问Client1并登陆之后,访问Client2,与访问Client1一样,先经过initialFlowSetupAction。
随后登录流程流转到第一个state(ticketGrantingTicketExistsCheck)。

<action-state id="ticketGrantingTicketCheck">
        <evaluate expression="ticketGrantingTicketCheckAction"/>
        <transition on="notExists" to="gatewayRequestCheck"/>
        <transition on="invalid" to="terminateSession"/>
        <transition on="valid" to="hasServiceCheck"/>
    </action-state>

因为已经登陆过,拥有请求的Cookie中存在有效的TGT,于是流程跳转到hasServiceCheck。

 <decision-state id="hasServiceCheck">
        <if test="flowScope.service != null" then="renewRequestCheck" else="viewGenericLoginSuccess"/>
    </decision-state>

判断是否是由应用页面跳转到登录页面登陆的,如果是,则跳转到generateServiceTicket,不是则跳转到viewGenericLoginSuccess。
此处跳转到renewRequestCheck。

 <decision-state id="renewRequestCheck">
        <if test="requestParameters.renew != '' and requestParameters.renew != null" then="serviceAuthorizationCheck"
            else="generateServiceTicket"/>
    </decision-state>

request中不存在renew,登录流程流转到第四个state(generateServiceTicket)。

 <action-state id="generateServiceTicket">
        <evaluate expression="generateServiceTicketAction"/>
        <transition on="success" to="warn"/>
        <transition on="unregisteredService" to="viewGenericLoginSuccess"/>
        <transition on="authenticationFailure" to="handleAuthenticationFailure"/>
        <transition on="error" to="initializeLogin"/>
        <transition on="gateway" to="gatewayServicesManagementCheck"/>
    </action-state>

后续的流转与应用系统webapp1相同,请参考前面webapp1的流转。

4.1.2 登出流程解析

登出的流程定义在logout-webflow.xml中。
首先访问登出接口/logout,流程跳转到terminateSession

 <action-state id="terminateSession">
    <evaluate expression="terminateSessionAction.terminate(flowRequestContext)" />
    <transition to="doLogout" />
  </action-state>

登出的方法主要调用路径如下:

TerminateSessionAction.terminate() 
--> CentralAuthenticationServiceImpl.destroyTicketGrantingTicket()
销毁TGT的方法
--> LogoutManagerImpl.performLogout() 
执行登出的方法,在该方法中向每个访问过的应用系统发送登出请求, 应用系统收到请求会销毁与用户的session
--> handleLogoutForSloService()
向应用系统发送登出请求的方法
--> performBackChannelLogout()   发送登出请求

terminateSessionAction.terminate()执行完毕之后,流程跳转到doLogout

 <action-state id="doLogout">
    <evaluate expression="logoutAction" />
    <transition on="finish" to="finishLogout" />
    <transition on="front" to="frontLogout" />
  </action-state>

 <decision-state id="finishLogout">
    <if test="flowScope.logoutRedirectUrl != null" then="redirectView" else="logoutView" />
  </decision-state>

  <end-state id="logoutView" view="externalRedirect:casLoginView" />

最终跳转到登录页面。

4.2 Client端源码解析

Cas client应用系统端通过几个Filter来实现登陆跳转和登出等功能。
下面以在web.xml中配置的几个Filter顺序来进行分析 。

4.2.1 SingleSignOutFilter

org.jasig.cas.client.session.SingleSignOutFilter是处理登出请求的Filter。该Filter判断是否是Cas Server端发过来的登出请求,如果是登出请求,则根据请求中的logoutMessage清除对应的Session。
登出请求的主要调用路径如下:

SingleSignOutFilter.doFilter()
--> SingleSignOutHandler.process()
--> destroySession(request)
--> session.invalidate();

4.2.2 AuthenticationFilter

org.jasig.cas.client.authentication.AuthenticationFilter是验证请求是否登陆过的Filter。

public final void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)servletRequest;
        HttpServletResponse response = (HttpServletResponse)servletResponse;
        // 判断该请求是否不需要验证,如果不需要,则跳转到下一个Filter
if(this.isRequestUrlExcluded(request)) {
            this.logger.debug("Request is ignored.");
            filterChain.doFilter(request, response);
        } else {
            HttpSession session = request.getSession(false);
           //从session中获取名为"_const_cas_assertion_"的Assertion 
 Assertion assertion = session != null?(Assertion)session.getAttribute("_const_cas_assertion_"):null;
           //如果存在,则说明已经登录,本过滤器处理完成,处理下个过滤器 
          if(assertion != null) {
                filterChain.doFilter(request, response);
            } else {
//生成serviceUrl  
                String serviceUrl = this.constructServiceUrl(request, response);
//从request中获取ST 
                String ticket = this.retrieveTicketFromRequest(request);
                boolean wasGatewayed = this.gateway && this.gatewayStorage.hasGatewayedAlready(request, serviceUrl);
                //如果ticket不为空,本过滤器处理完成,处理下个过滤器 
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);
                  //生成重定向URL 
                    String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl, this.getProtocol().getServiceParameterName(), modifiedServiceUrl, this.renew, this.gateway);
                    this.logger.debug("redirecting to \"{}\"", urlToRedirectTo);
                    String reqType = request.getHeader("X-Requested-With");
                    //如果是异步请求,则返回410状态码给前端
                    if("XMLHttpRequest".equalsIgnoreCase(reqType)) {
                        String json = "{\"flag\":0,\"error\":401,\"data\":{}}";
                        response.getWriter().write(json);
                    } else {
                    //跳转到CAS服务器的登录页面 
                        this.authenticationRedirectStrategy.redirect(request, response, urlToRedirectTo);
                    }

                } else {
                    filterChain.doFilter(request, response);
                }
            }
        }
    }

当我们从浏览器访问配置了单点登录的应用系统时(http://www.client1.com/index),由于集成了CAS单点登录客户端,此时进入到第一个过滤器AuthenticationFilter(不考虑其他非单点登录的过滤器),执行以下操作:
1 从session中获取名为“const_cas_assertion”的assertion对象,判断assertion是否存在,如果存在,说明已经登录,执行下一个过滤器。如果不存在,执行第2步。
2 生成serviceUrl(http://www.client1.com/index),从request中获取票据参数ticket,判断ticket是否为空,如果不为空执行下一个过滤器。如果为空,执行第3步。
3 生成重定向URL,如:
http://www.casserver.com/login?service=http://www.client1.com/index
4 跳转到单点登录服务器,显示登录页面,此时第一个过滤器执行完成。

4.2.3 ticketValidationFilter

org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter为验证Service Ticket的Filter。
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中获取参数 
            String ticket = this.retrieveTicketFromRequest(request);
            //ticket不为空,验证ticket,否则本过滤器处理完成,处理下个过滤器
            if(CommonUtils.isNotBlank(ticket)) {
                this.logger.debug("Attempting to validate ticket: {}", ticket);

                try {
//验证ticket并产生Assertion对象,错误抛出TicketValidationException异常 
  Assertion assertion = this.ticketValidator.validate(ticket, this.constructServiceUrl(request, response));
             this.logger.debug("Successfully authenticated user: {}", assertion.getPrincipal().getName());
                   //给request设置assertion
                   request.setAttribute("_const_cas_assertion_", assertion);
                   //给session设置assertion  
                   if(this.useSession) {
                        request.getSession().setAttribute("_const_cas_assertion_", assertion);
                    }

                    this.onSuccessfulValidation(request, response, assertion);
                    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);
        }
    }

假设当执行完第一个过滤器后,跳转到CAS服务器端的登录页面,输入用户名和密码,验证通过后。CAS服务器端会生成ticket,并将ticket作为重新跳转到应用系统的参数(http://www.client1.com/index?ticket=ST-1-4hH2s5tzsMGCcToDvGCb-cas01.example.org)。此时又进入第一个过滤器AuthenticationFilter,由于存在ticket参数,进入到第二个过滤器TicketValidationFilter,执行以下操作:
1 从request获取ticket参数,如果ticket为空,继续处理下一个过滤器。如果参数不为空,验证ticket参数的合法性。
2 验证ticket,TicketValidationFilter的validate方法通过httpClient访问CAS服务器端(http://www.casserver.com/serviceValidate?ticket=ST-1-4hH2s5tzsMGCcToDvGCb-cas01.example.org&service=http://www.client1.com/index)验证ticket是否正确,并返回assertion对象。如果验证失败,抛出异常,跳转到错误页面。如果验证成功,session会以"const_cas_assertion"的名称保存assertion对象,继续处理下一个过滤器。

4.2.4 HttpServletRequestWrapperFilter

org.jasig.cas.client.util.HttpServletRequestWrapperFilter对HttpServletRequest对象再包装一次,让其支持getUserPrincipal,getRemoteUser方法来取得登录的用户信息。

public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { 
 //从Session或者request中取得AttributePrincipal,其实Assertion的一个principal属性 
AttributePrincipal principal = this.retrievePrincipalFromSessionOrRequest(servletRequest);
//对request进行包装,并处理后面的过滤器,使其后面的过滤器或者servlet能够在request.getRemoteUser()或者request.getUserPrincipal()取得用户信息  
filterChain.doFilter(newHttpServletRequestWrapperFilter.CasHttpServletRequestWrapper((HttpServletRequest)servletRequest, principal), servletResponse); }

5 常见问题

6 其他

6.1 Gradle相关

6.1.1 Gradle 打包实现生产环境与测试环境配置分离

在build.gradle中加入以下代码

#默认情况下为ent-dev
def env = System.getProperty("profile") ?: "ent-dev"
sourceSets {
    main {
        resources {
            srcDirs = ["src/main/resources", "src/main/$env"]
        }
    }
}

在/src/main目录下建立各个环境目录,如 ent-dev、ent-prod等。
对于cas系统,配置参数都在cas.properties中,可以将cas.properties放入各个环境目录中, 并修改读取cas.properties路径。
修改WEB-INF/spring-configuration目录中的propertyFileConfigurer.xml

 <util:properties id="casProperties" location="${cas.properties.config.location:/WEB-INF/cas.properties}"/>

修改为

<util:properties id="casProperties" location="${cas.properties.config.location:classpath:/cas.properties}"/>

build生产环境时可使用命令:gradle build -D profile=ent-prod

6.2 调用263和LDAP验证用户名密码步骤

6.2.1 在项目中添加依赖

在项目build.gradle中加入以下内容,如果是Maven项目,则在pom.xml文件中加入maven格式依赖。

shangdeCommonSdfVersion=0.1.0.5-ENT-SNAPSHOT
compile group: 'com.xxx.common', name: 'sdf-common-util', version: shangdeCommonSdfVersion
compile group: 'com.xxx.common', name: 'sdf-common-web', version: shangdeCommonSdfVersion
compile group: 'com.xxx.common', name: 'sdf-common-auth-web', version: shangdeCommonSdfVersion
compile group: 'com.xxx.common', name: 'sdf-common-sys', version: shangdeCommonSdfVersion
compile group: 'com.xxx.common', name: 'sdf-common-authentication-263', version: shangdeCommonSdfVersion
compile group: 'com.xxx.common', name: 'sdf-common-auth', version: shangdeCommonSdfVersion

6.2.2 在properties文件中添加以下内容

263.domain=xyz.com
263.account=xyz.com
263.key=Zs54D6jo#
263.webServiceUrl=http://macom.263.net/axis/xmapi
ldap.url=ldap://172.16.117.215:389

6.2.3 调用验证方法

@Autowired
private SdfPasswordValidatorImpl sdfPasswordValidatorImpl;
// 返回值为true及验证成功
boolean isValidUser = sdfPasswordValidatorImpl.authenticate(username,password);



作者:Ferrari1001
链接:https://www.jianshu.com/p/3dcaaa12e976
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值