单点登录(二):原理代码分析

来源:互联网 发布:7u分享网络微信打不开 编辑:程序博客网 时间:2024/04/30 05:35
主要分析:
用户第一次发送请求--->CAS客户端转发至CAS服务器---->CAS服务器返回登录页--->用户登录(输入帐号密码)发送至CAS服务器----->CAS服务器认证------>重定向目标资源。

一.用户第一次发送请求----->CAS客户端转发
     1.1 用户首次访问,过滤器拦截
     1.2 过滤器doFilter()具体操作
     1.3 过滤器转发操作
     1.4 请求路径
二.CAS客户端转发---->CAS服务器返回登录页
     2.1 请求交给springmvc来进行处理
     2.2 springmvc 把请求交给工作流Webflow进行处理
     2.3工作流处理请求流程
三.用户登录(输入帐号密码)发送----->CAS服务器认证
     3.1 login-webflow.xml 指定类进行处理
     3.2 AuthenticationViaFormAction的具体认证过程
四.CAS服务器认证------>重定向目标资源

一.用户第一次发送请求----->CAS客户端转发
1.1 用户首次访问,过滤器拦截
用户第一次访问子系统A的时候,由于在子系统A配置了过滤器,所以首先会对请求进行拦截,如下web.xml代码:
            <filter>
                <filter-name>CAS Filter</filter-name>
                <filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>
                <init-param>
                    <param-name>casServerLoginUrl</param-name>
                    <param-value>https://iot:8443/casServer/login</param-value>
                </init-param>
                <init-param>
                    <param-name>serverName</param-name>
                    <param-value>http://iot:18080</param-value>
                </init-param>
            </filter>
            <filter-mapping>
                <filter-name>CAS Filter</filter-name>
                <url-pattern>/*</url-pattern>
            </filter-mapping>

1.2 过滤器doFilter()具体操作
org.jasig.cas.client.authentication.AuthenticationFilter类中,执行其doFilter()方法。在该方法中,判断是系统存在其对的session,如果存在,则不转发至CAS服务器中,如下代码:
        final HttpSession session = request.getSession(false);
        final Assertion assertion = session != null ? (Assertion) session.getAttribute(CONST_CAS_ASSERTION) : null;
        if (assertion != null) {
            filterChain.doFilter(request, response);
            return;
        }

1.3 过滤器转发操作
3.如果不存在的话,进行转发,与此同时,把用户请求的网址附带一起发送给CAS服务器,如下doFilter()中的代码:
final String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl, getServiceParameterName(), modifiedServiceUrl, this.renew, this.gateway);
response.sendRedirect(urlToRedirectTo);

如图所示:

1.4 请求路径
该urlToRedirectTo路径究竟是哪个呢?
我们可以看到在AuthenticationFilter类中有一个this.casServerLoginUrl的变量,在AuthenticationFilter类中,也就是我们的过滤器处理类中,有initInternal()方法,该方法看字面意思是一个初始化的方法,里面有一个setCasServerLoginUrl(getPropertyFromInitParams(filterConfig, "casServerLoginUrl", null));方法,该方法
该方法里面的getPropertyFromInitParams(filterConfig, "casServerLoginUrl", null);看到filterConfig变量,根据过滤器的的知识,我们就可以知道,是用filterConfig来取web.xml中的初始化变量,再回顾上面的web.xml文件,配有变量
<filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>
                <init-param>
                    <param-name>casServerLoginUrl</param-name>
                    <param-value>https://iot:8443/casServer/login</param-value>
                </init-param>
所以我们可以知道,是请求CAS服务器的https://iot:8443/casServer/login 地址。

最后,我们首先可以得到结论,用户所有请求首先会被CAS客户端拦截,并且第一步判断其有无对应session,有则直接可以访问目标资源,如果没有,则需要转向至CAS服务器。

二.CAS客户端转发---->CAS服务器返回登录页
2.1 请求交给springmvc来进行处理
在CAS中MVC的控制主要是使用的spring MVC来实现的。但是,在登录过程中,因为有点类似于工作流的性质,,采用了一个轻量级的工作流框架,就是spring 的weflow
首先,在CAS服务器的web.xml入口文件中,有以下两端代码,可以我们可以知道,首先请求是给了org.jasig.cas.web.init.SafeDispatcherServlet来进行处理。
    <servlet-name>cas</servlet-name>
    <servlet-class>
      org.jasig.cas.web.init.SafeDispatcherServlet
    </servlet-class> 
<servlet-mapping>
    <servlet-name>cas</servlet-name>
    <url-pattern>/login</url-pattern>
  </servlet-mapping>

那么,在org.jasig.cas.web.init.SafeDispatcherServlet类中:
public void service(final ServletRequest req, final ServletResponse resp)
        throws ServletException, IOException {
        if (this.initSuccess) {
            this.delegate.service(req, resp);
        } else {
            throw new ApplicationContextException(
                "Unable to initialize application context.");
        }
    }

2.2springmvc 把请求交给工作流Webflow进行处理
我们可以知道,其实最后也是交给了springmvc了处理。那么springmvc究竟如何处理?

Spring MVC核心配置文件是cas-servlet.xml。在该文件中,webflow将与springMVC进行集成。这里有一个问题,就是spring何时开始加载cas-servlet.xml文件的呢?原来,在初始化DispatcherServlet的时候,会自动加载 servlet-name+“-servlet.xml”文件。所以,cas-servlet.xml是自动加载的,不需要在配置文件进行配置。(参见关于springMVC的文章)

交给spring MVC之后,spring MVC又将请求交给了 webflow处理
在cas-servlet.xml文件中,我们可以看到,有下面一段webflow定义:

  <webflow:flow-executor id="flowExecutor" flow-registry="flowRegistry">
    <webflow:flow-execution-attributes>
      <webflow:always-redirect-on-pause value="false"/>
      <webflow:redirect-in-same-state value="false" />
    </webflow:flow-execution-attributes>
    <webflow:flow-execution-listeners>
      <webflow:listener ref="terminateWebSessionListener" />
    </webflow:flow-execution-listeners>
  </webflow:flow-executor>
  <webflow:flow-registry id="flowRegistry" flow-builder-services="builder">
    <webflow:flow-location path="/WEB-INF/login-webflow.xml" id="login"/>
  </webflow:flow-registry>
  <webflow:flow-builder-services id="builder" view-factory-creator="viewFactoryCreator"
                                 expression-parser="expressionParser"/>

在该文件中,我们可以看到上面的配置项。这就是将webflow框架作为spring MVC的一个节点来进行配置。webflow:flow-registry节点就是注册了一个webflow流程,该流程的入口,也就是ID=“login”。这样,交给springMVC的请求路径如果是login的,则有springMVC交给webflow处理。

在webflow中,会定义一些视图,这些视图都是以view=”XXX”的形式存在的。那么XXX又是如何找到对应的页面呢??看flow-builder-services节点,我们会发现有个view-factory-creator属性,该属性就定义了视图解析工厂。

该视图解析工厂是由视图解析器组成的。这里只定义了一个视图解析器,就是viewResolvers。该视图解析器是springFramework中的ResourceBundleViewResolver的一个实例,该类可以通过basenames属性,找到value值对应的properties属性文件,该文件中式类似ke=values类型的内容,正是该文件将jsp文件映射成视图名称。

至此,springMVC与webflow已经集成完毕。


2.3工作流处理请求流程
那么在/WEB-INF/login-webflow.xml文件中,
1.首先判断有无TGT,没有所以去到gatewayRequestCheck节点中,如下代码:
    <decision-state id="ticketGrantingTicketExistsCheck">
        <if test="flowScope.ticketGrantingTicketId != null" then="hasServiceCheck" else="gatewayRequestCheck" />
    </decision-state>
2.然后再判断有没有requestParameters.gateway,首次访问flowScope.service为false,那么就会来到serviceAuthorizationCheck节点,如下代码:
    <decision-state id="gatewayRequestCheck">
        <if test="requestParameters.gateway != '' and requestParameters.gateway != null and flowScope.service != null" then="gatewayServicesManagementCheck" else="serviceAuthorizationCheck" />
    </decision-state>
3.接着在serviceAuthorizationCheck,转去用generateLoginTicket节点进行处理,如下代码:
    <action-state id="serviceAuthorizationCheck">
        <evaluate expression="serviceAuthorizationCheck"/>
        <transition to="generateLoginTicket"/>
    </action-state>
4.接着在generateLoginTicket中,使用generateLoginTicketAction类中的genetate()方法进行处理。处理完后,如果返回"genetated",则进入viewLoginForm节点,如下代码:
    <action-state id="generateLoginTicket">
        <evaluate expression="generateLoginTicketAction.generate(flowRequestContext)" />
        <transition on="generated" to="viewLoginForm" />
    </action-state>
5.接着在generateLoginTicketAction类在cas-servlet.xml文件中有配置,比如:
<bean id="generateLoginTicketAction" class="org.jasig.cas.web.flow.GenerateLoginTicketAction"  p:ticketIdGenerator-ref="loginTicketUniqueIdGenerator"/>
这个类会为该请求生成一个流水号,并且返回"generated"。Ps:必须要这个流水号,下次用户输入帐号密码登录时,CAS服务器会首先判断有没有这个流水号。

6.最后,很据<transition on="generated" to="viewLoginForm" />,进入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>
如上所示,最后返回了casLoginView登录页。

整个过程如图:

.用户登录(输入帐号密码)发送----->CAS服务器认证
3.1 login-webflow.xml 指定类进行处理
当用户登录时候,页面上有form表单,<form:form method="post" id="fm1" cssClass="fm-v clearfix" commandName="${commandName}" htmlEscape="true">这里我们看不出会响应什么,但是,我们可以看之前的login-webflow.xml,如下代码:
    <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>
由上,会转向realSubmit节点,在realSubmit中:
<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>
可以看到,log-webflow.xml指定authenticationViaFormAction.submit函数对用户名密码进行认证。

3.2AuthenticationViaFormAction的具体认证过程
3.2.1 验证流水号
去到这个类的这个类的这个submit函数中,首先会验证ticket,也就是我们之前说的流水号,必须要有。
        // Validate login ticket
        final String authoritativeLoginTicket = WebUtils.getLoginTicketFromFlowScope(context);
        final String providedLoginTicket = WebUtils.getLoginTicketFromRequest(context);
        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";
        }

3.2.1 验证帐号密码
在这个submit方法的最后,有一个语句,这个语句就是完成认证的:
WebUtils.putTicketGrantingTicketInRequestScope(context, this.centralAuthenticationService.createTicketGrantingTicket(credentials));
其中credentials包含了用户输入的信息,比如用户名,密码等。
在this.centralAuthenticationService.createTicketGrantingTicket(credentials)完成认证,认证不过是会抛异常的。

注意:
1. this.centralAuthenticationService 由 private CentralAuthenticationService centralAuthenticationService; 声明。
2.CentralAuthenticationService 是一个接口,CentralAuthenticationServiceImpl是其实现类。
3.所以说this.centralAuthenticationService.createTicketGrantingTicket(credentials)这个方法应该是实现类CentralAuthenticationServiceImpl的一个方法。

去到这个类的这个方法,看看是怎么实现的。
    public String createTicketGrantingTicket(final Credentials credentials) throws TicketCreationException {
        Assert.notNull(credentials, "credentials cannot be null");
        try {
            final Authentication authentication = this.authenticationManager
                .authenticate(credentials);
            final TicketGrantingTicket ticketGrantingTicket = new TicketGrantingTicketImpl(
                this.ticketGrantingTicketUniqueTicketIdGenerator
                    .getNewTicketId(TicketGrantingTicket.PREFIX),
                authentication, this.ticketGrantingTicketExpirationPolicy);

            this.ticketRegistry.addTicket(ticketGrantingTicket);
            return ticketGrantingTicket.getId();
        } catch (final AuthenticationException e) {
            throw new TicketCreationException(e);
        }
    }
由上述红色代码可以知道,第一,验证帐号密码,第二,创建TGT,并且缓存TGT。以便在后面如果用户在其他子系统登录的时候根据这个缓存了的值就不需要用户再登录了。
对于缓存TGT:是通过HashMap实现缓存,有兴趣可以 深入跟踪this.ticketRegistry.addTicket(ticketGrantingTicket);
验证final Authentication authentication = this.authenticationManager.authenticate(credentials);             
注意:
this.authenticationManager
.authenticate(credentials);中的this.authenticationManager在本类中没有定义,但是有其注入函数,注释可知是由springmvc注入,如下代码:
/**   
     * Method to inject the AuthenticationManager into the class.
     *
     * @param authenticationManager The authenticationManager to set.
     */
    public void setAuthenticationManager(
        final AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }
可以看出,该实例其实是通过springmvc注入的。在deployConfigContext.xml中有其定义,如下代码:
<bean id="authenticationManager" class="org.jasig.cas.authentication.AuthenticationManagerImpl">
<property name="credentialsToPrincipalResolvers">
    <property name="authenticationHandlers">
            <list>
                    <bean class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler" p:httpClient-ref="httpClient" />
                    <bean class="org.jasig.cas.authentication.handler.support.SimpleTestUsernamePasswordAuthenticationHandler" />
            </list>
     </property>
</property>
</bean>
由上代码可知道,先把SimpleTestUsernamePasswordAuthenticationHandler注入到AuthenticationManagerImpl,再把AuthenticationManagerImpl注入到其CentralAuthenticationServiceImpl 进行验证。
Ps:在SimpleTestUsernamePasswordAuthenticationHandler类中,有如下具体验证代码:
if (StringUtils.hasText(username) && StringUtils.hasText(password)
            && username.equals(getPasswordEncoder().encode(password)))

总结:


当认证成功之后,一开始的submit方法就会返回"success。再接着会返回重定向到目标页面。

四.CAS服务器认证------>重定向目标资源
认证成功后,后续就非常简单了。在一开始的AuthenticationViaFormAction中的realSubmit函数,刚才我们着重分析了WebUtils.putTicketGrantingTicketInRequestScope(context, this.centralAuthenticationService.createTicketGrantingTicket(credentials));这句话的认证过程。
那么这句话认证通过后,就会执行下面的两句话,
WebUtils.putTicketGrantingTicketInRequestScope(context, this.centralAuthenticationService.createTicketGrantingTicket(credentials));
1.putWarnCookieIfRequestParameterPresent(context);
2.return "success";
在return "success"后,在login-webflow中有<transition on="success" to="sendTicketGrantingTicket" />,又会去到其他的函数,相应的设置浏览器的cookie,Web服务器的session等东西。


1 0
原创粉丝点击