原帖: http://blog.csdn.net/sjbup/article/details/7456385 http://blog.csdn.net/sjbup/article/details/7468466 http://blog.csdn.net/sjbup/article/details/7468472
受益匪浅,特转发以示敬意~~ =================================================
在CAS中MVC的控制主要是使用的spring MVC来实现的。但是,在登录过程中,因为有点类似于工作流的性质,所以,采用了一个轻量级的工作流框架,就是spring 的weflow。下面,我们就CAS如何采用webflow控制登录流程进行分析。
想深入理解webflow工作原理的读者需要参考官方的webflow2.21版本的reference。
好了,话不多说,开始CAS认证流程之旅。
用户请求如何进入认证流程的 用户一开始输入http://localhost:8080/CASServer 的时候,默认是回去访问 index.jsp的。我们看一下index.jsp,里面的内容非常简单。
final String queryString = request.getQueryString();
final String url = request.getContextPath() +"/login" + (queryString!= null ?"?" + queryString : "");
response.sendRedirect(response.encodeURL(url));
这里进行的处理就是将用户的请求重定向到CAS认证中心的 /login 路径下。在上一章中,我们已经知道,该路径我们是由 名为 CAS 的servlet,也就是org.jasig.cas.web.init.SafeDispatcherServlet 进行处理的。我们进入该servlet看一下他是如何进行处理的。
进入该servlet中,我们看到,他继承自 HttpServlet,也就是说他只是一个普通的servlet。该servlet持有一个DispatcherServlet 属性delegate。这同struts的处理方式如出一辙。都是通过一个代理类来进行处理请求的。
该类的初始化也仅仅是调用代理类delegate的初始化。Service函数很仅仅是调用delegate的service函数。
看来,DispatcherServlet类才是处理请求的关键类。我们看到DispatcherServlet类的完整路径是
org.springframework.web.servlet.DispatcherServlet
这里,也就是将请求交给了spring MVC处理了。(关于spring mvc,参考我其他的关于spring 的源码分析文章)。
Webflow与Spring MVC集成 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处理。下面是webflow同spring MVC的结合:
<!-- 根据工作流定义,生成一个执行器 -->
<webflow:flow-executorid="flowExecutor"flow-registry="flowRegistry">
<webflow:flow-execution-attributes>
<webflow:always-redirect-on-pausevalue="false"/>
</webflow:flow-execution-attributes>
</webflow:flow-executor>
<!-- 注册一个工作流 id是子路径 为flow入口对login的请求交由login-webflow.xml定义的处理器进行处理-->
<webflow:flow-registryid="flowRegistry"flow-builder-services="builder">
<webflow:flow-locationpath="/WEB-INF/login-webflow.xml"id="login"/>
</webflow:flow-registry>
<webflow:flow-builder-servicesid="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已经集成完毕。
Webflow配置文件及源码分析 在WEB-INF文件夹下的login-webflow.xml是登陆流程的主要配置文件。在该文件中,定义了用户登录的整个处理流程。
首先,配置文件中的 on-start标签定义了用户第一次进入流程中的预处理动作。该标签对应spring中的id为initialFlowSetupAction的bean。
查看该bean(InitialFlowSetupAction)的代码。该类需要继承自AbstractAction,AbstractAction方法是org.springframework.webflow.action包中的类。是webflow中的基础类。该类中的doExecute方法是对应处理业务的方法。就犹如servlet中的service方法一样。该方法的参数是RequestContext对象,该参数是一个流程的容器。该方法从request中获取TGT,并且构建一个临时的service对象(不同域注册的service,详情见接入系统管理)。并且,将TGT和service放在FlowScope作用域中。
流程的初始化完毕之后,就开始一系列的判断了。也就是进入decision-state节点。这些节点是依次执行的。
<!--检查flow中是否存在TGT如果存在,存在进入hasServiceCheck,为空进入gatewayRequestCheck-->
<decision-stateid="ticketGrantingTicketExistsCheck">
<if test="flowScope.ticketGrantingTicketIdneq null"then="hasServiceCheck"else="gatewayRequestCheck"/>
</decision-state>
<!-- 主要是CS结构使用gatewat,暂时不研究-->
<decision-stateid="gatewayRequestCheck">
<if test="externalContext.requestParameterMap['gateway']neq '' && externalContext.requestParameterMap['gateway'] neqnull && flowScope.service neq null" then="gatewayServicesManagementCheck"else="viewLoginForm"/>
</decision-state>
<!-- 存在TGT,说明用户已经登陆,测试flow中service是否为空,不为空,进入renewRequestCheck,为空,进入viewGenericLoginSuccess-->
<decision-stateid="hasServiceCheck">
<if test="flowScope.service!= null"then="renewRequestCheck"else="viewGenericLoginSuccess"/>
</decision-state>
<!--
用户已经登陆,且请求参数中存在service判断请求中是否存在'renew'参数,如果renew参数为空或者没有内容,那么,进入viewLoginForm,否则进入generateServiceTicket
renew参数和gateway参数不兼容。renew参数将绕过单点登录。也就是说即使用户登录了,还将要求用户登录。(等你妹啊,人家都登录了,凭什么还要让人家再登录一次)
-->
<decision-stateid="renewRequestCheck">
<if test="externalContext.requestParameterMap['renew']neq '' && externalContext.requestParameterMap['renew'] neqnull"then="viewLoginForm"else="generateServiceTicket"/>
</decision-state>
<decision-stateid="warn">
<if test="flowScope.warnCookieValue"then="showWarningView"else="redirect"/>
</decision-state>
对应的dicision-state走完之后,如果不存在TGT,其实就会进入voiwLoginForm节点。该节点是一个view-state类型的,这也就是说明该节点是一个页面,view=“casLoginView”属性定义了该view对应的页面是“casLoginView”。这个视图会被spring的视图解析器解析成/WEB-INF/view/jsp/default/ui/casLoginView.jsp页面。用户这时候就能看到一个登陆界面了。
需要注意的是,用户看到的登录界面中,会有hidden类型的一个lt参数:
<inputtype="hidden" name="lt"value="${flowExecutionKey}" />
该参数可以理解成每个需要登录的用户都有一个流水号。只有有了webflow发放的有效的流水号,用户才可以说明是已经进入了webflow流程。否则,没有流水号的情况下,webflow会认为用户还没有进入webflow流程,从而会重新进入一次webflow流程,从而会重新出现登录界面。
用户点击登录之后,提交到realSubmit节点。该节点执行的是authenticationViaFormAction.submit方法,在该方法中,将会验证用户的认证信息是否正确。验证成功之后,跳转到sendTicketGrantingTicket,在这里,将生成TGT,然后,进入serviceCheck,在serviceCheck中,将会验证flowScope作用域中是否存在service,如果存在,则进入generateServiceTicket,否则进入登录成功页面。在generateServiceTicket中,也将生成ST,同时检查是否有警告信息,然后进行重定向到用户最开始请求的地址。
至此,springMVC与webflow整合以及登录整个流程已经讲解完毕。
这里讲解的比较粗略。后续可能会较详细的写一下webflow的工作原理。另外,如果对于自定义登录流程如何处理,将会在特殊场景的解决方案中给出。 -------------------------------------------- 上一部分主要是对webflow同spring MVC进行结合进行了粗略的讲解。这里,将对webflow定义的流程进行更加详细的说明。
前面说到用户的认证请求经过spring MVC 对应配置的/login 路径进入webflow中的viewLoginForm 也就是登录界面。该用户的登录界面通过
<transition on="submit" bind="true" validate="true" to="realSubmit">
<set name="flowScope.credentials"value="credentials" />
<evaluate expression="authenticationViaFormAction.doBind(flowRequestContext,flowScope.credentials)" />
</transition>
用户点击登录之后,就将登录请求提交到了realSubmit中。realSubmit执行的对应的是AuthenticationViaFormAction中的submit方法。在该方法中,首先通过
final String ticketGrantingTicketId =WebUtils.getTicketGrantingTicketId(context); 判断用户请求中,是否存在TGT(Ticket grant ticket)。这里说明一下,TGT是以cookie形式存在CASServer域下的。默认情况是只有http请求才向CASServer提交的。该cookie为CASServer识别用户是否登录的唯一标示。如果存在,则认为是用户已经登陆过的,否则认为用户是没有登录过的。
然后通过final Service service =WebUtils.getService(context);获取service。这个service是对应的服务。就是在你访问某个应用程序的时候,该请求被统一认证的过滤器拦截之后跳转到统一认证进行认证。跳转的过程中,会把你之前的请求的路径记录下来并作为参数传递到统一认证。在统一认证系统中配置的所有可以使用统一认证系统的业务系统的配置信息。该认证请求到了统一认证之后,统一认证根据这个路径查找对应的系统的配置信息。详情参照系统管理部分。
如果不存在TGT,则通过下面方法新生成一个TGT。
String tempTGT= this.centralAuthenticationService.createTicketGrantingTicket(credentials);
如果TGT存在,则生成ST并将其放入到context中。ST是service ticket的缩写。顾名思义,该ticket是和service对应的。所以,如果验证ticket有效性的时候,发送的验证请求所带service参数同生成ST的时候的参数不一致,就会出现ticket异常。默认的,ST的有效性是有时间和次数的限制的。默认是ST使用一次就失效,在1000秒以后失效。详情参见《CAS中ticket的生成与管理》。
final String serviceTicketId = this.centralAuthenticationService.grantServiceTicket(ticketGrantingTicketId, service,credentials);
WebUtils.putServiceTicketInRequestScope(context, serviceTicketId);
putWarnCookieIfRequestParameterPresent(context);
return "warn";
在webflow中跳转到“warn”状态,该状态中主要是验证流程中是否存在warnCookie,如果存在,则跳转到showWarningView中,否则进入redirect。在往下就是跳转到认证之前的目标路径了。注意,这里跳转的过程中,将ST作为跳转路径中的一个参数进行传递了。 ------------------------------------- 这篇文章主要是对用户提交的用户名及密码等进行认证。该过程相对简单一些,但我们更多的是学习他的设计思路。
上一篇文章中,我们在authenticationViaFormAction 中的submit方法中TGT的生成是通过
String tempTGT= this.centralAuthenticationService.createTicketGrantingTicket(credentials);方法来生成的。
在该方法中,参数credentials就是对用户提交的认证信息的封装,该对象主要就是简单的包含了用户名和密码等。
那我们进详细的看一看该方法中逻辑行为吧。
CentralAuthenticationService 接口是CAS中的核心接口,他定了CAS中核心的一些行为。
createTicketGrantingTicket
该方法主要是验证认证信息,生成TGT。
grantServiceTicket 该方法主要是生成ST
validateServiceTicket 该方法主要是验证ST有效性。
destroyTicketGrantingTicket 该方法主要是销毁TGT
delegateTicketGrantingTicket 该方法主要是代理票据的生成。
可以看到,这些都是认证流程中的核心方法。
该接口的默认实现是CentralAuthenticationServiceImpl
我们先来看一下createTicketGrantingTicket方法
final Authenticationauthentication = this.authenticationManager.authenticate(credentials);
finalTicketGrantingTicket ticketGrantingTicket= new TicketGrantingTicketImpl(
this.ticketGrantingTicketUniqueTicketIdGenerator
.getNewTicketId(TicketGrantingTicket.PREFIX),
authentication, this.ticketGrantingTicketExpirationPolicy);
this.ticketRegistry.addTicket(ticketGrantingTicket);
return ticketGrantingTicket.getId();
这里可以看到, 代码中先是通过调用this.authenticationManager.authenticate方法对credentials进行验证。并返回验证结果 authentication。在这个过程中,如果验证出现问题,是会抛出异常的。
然后通过构建一个TicketGrantingTicketImpl 对象来形成TGT对应信息。之前说过,TGT是以cookie的形式存在的。那是在客户端浏览器中存在形式。在server端测试存在一个ticketGrantTicket对象的。这一点同session与jessionid的关系非常像。生成的tgt存储在ticketRegistry中。然后返回TGT。这里先不对ticket的生成和管理进行讲解,后面会详细的说明,这里继续讲解认证的过程。
this.authenticationManager.authenticate(credentials);
我们要看一下该方法中的认证是怎么样的,首先要看一下这个类的结构是什么样的。CAS中对象的管理主要是通过spring的IOC实现的。关于认证的bean的配置主要是在deployConfigContext.xml中定义的。在该配置文件中
<bean id="authenticationManager"
class="org.jasig.cas.authentication.AuthenticationManagerImpl">
<!--credentialsToPrincipalResolvers主要是干两件事儿,
一是确认用户要授权,在默认配置中用的DefaultCredentialsToPrincipalResolver来填充角色
二是用这些分解器确认一个服务请求代理票据验证。这个需要明确的是代理登录流程 +-->
<property name="credentialsToPrincipalResolvers">
<list>
<!--这个UsernamePasswordCredentials是支持用户名密码登录的解析器。如果采用其他的认证方式,那么需要替换解析器。这种解析器需要支持这种认证方式
+-->
<bean class="org.jasig.cas.authentication.principal.UsernamePasswordCredentialsToPrincipalResolver">
<property name="attributeRepository">
<ref local="attributeRepository"/>
</property>
</bean>
<!--这个类是表示支持http或是https协议的。他可以从URL获取凭证,并且可以根据回调地址获取服务地址。如果采用其他的通讯协议,可以进行相应的替换+-->
<bean class="org.jasig.cas.authentication.principal.HttpBasedServiceCredentialsToPrincipalResolver"/>
</list>
</property>
<!--这里的list主要是维护了一个认证器链,用来对用户提交的认证信息进行认证。
+-->
<property name="authenticationHandlers">
<list>
<bean class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler"
p:httpClient-ref="httpClient"/>
<bean class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler">
<property name="dataSource"ref="dataSource" />
<property name="sql"value="select USER_PASSWORD fromt_sso_userinfo where USER_NAME=? " />
</bean>
</list>
</property>
</bean>
我们可以看到,在manager中,主要有两个list的属性:credentialsToPrincipalResolvers和authenticationHandlers
其中authenticationHandlers list主要是用来做认证用的。该list中所有的bean都需要实现AuthenticationHandler接口中的authenticate方法。用户提交认证请求之后,要满足该list中任意认证的条件才算是认证成功。每一个bean都可以配置自己的验证方式。所以,对于有多个认证方式的应用的时候,在这里自行组装认证条件就可以。
credentialsToPrincipalResolverslist属性主要是在验证成功之后,将用户属性提取出来。并传递到接入系统中。如果用户数据存储在存在多个数据源中,则在这里可以写多个属性提取器,分别将用户的属性提取出来,然后处理后传递给客户端。
下面我们就源码进行分析。查看AuthenticationManagerImpl.authenticateAndObtainPrincipal方法:
//通过验证器链对用户提供的认证信息进行认证
for (final AuthenticationHandlerauthenticationHandler : this.authenticationHandlers) {
if(authenticationHandler.supports(credentials)) {
foundSupported = true;
if(!authenticationHandler.authenticate(credentials)) {
。。。。。
} else {
。。。。。
authenticatedClass =authenticationHandler;
authenticated = true;
break;
}
}
}
这里可以看到,就是对authenticationHandlers 进行遍历,如果通过认证,则break。
foundSupported = false;
//认证成功之后,通过认证信息,提取出用户的完整信息
for (finalCredentialsToPrincipalResolver credentialsToPrincipalResolver : this.credentialsToPrincipalResolvers) {
if(credentialsToPrincipalResolver.supports(credentials)) {
final Principal principal =credentialsToPrincipalResolver
.resolvePrincipal(credentials);
foundSupported = true;
if (principal != null) {
return new Pair<AuthenticationHandler,Principal>(authenticatedClass,principal);
}
}
}
这里对credentialsToPrincipalResolvers进行解析,一旦获取到用户的属性,则构建了一个新的Pair对象,该对象中有认证的类和用户属性。
下面,我们就深入org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler看一下该类是如何对用户进行认证的。
这里,我对这个类简单看一下,credentials是对认证请求的简单封装。当然可以获取到用户名和密码。然后将密码进行加密之后,同数据库查询的结果进行比对。
final Stringusername = getPrincipalNameTransformer().transform(credentials.getUsername());
final String password =credentials.getPassword();
final String encryptedPassword = this.getPasswordEncoder().encode(
password);
try {
final String dbPassword =getJdbcTemplate().queryForObject(
this.sql, String.class,username);
returndbPassword.equals(encryptedPassword);
} catch (finalIncorrectResultSizeDataAccessException e) {
// this means the username was notfound.
return false;
}
CAS中,对于数据库的操作主要都是使用spring的JdbcTemplate 来操作的。这里也不例外。
这里的业务逻辑比较简单。在回头看看这里的实现。他将能分离出来的组件全都独立出来。比如说密码加密算法。数据源,查询语句等。任何组件的更改不会对其他的组件产生影响。这就是面向对象的好处,这就是使用spring ioc的好处。
|