前期准备:
3.应用系统webapp(http://127.0.0.1:8090/webapp/main.do)
4.CAS单点登录服务器端(http://127.0.0.1:8081/cas-server/)
<filter>
<filter-name>CASFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter>
<filter-name>CASValidationFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter>
<filter-name>initLoginUserFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>CASFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>CASValidationFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>initLoginUserFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
spring-context.xml部分代码
<bean id="CASFilter" class="com.master.client.filter.AuthenticationFilter" init-method="initConfig"/>
<bean id="CASValidationFilter" class="com.master.client.filter.TicketValidationFilter" init-method="initConfig"/>
<bean id="initLoginUserFilter" class="com.master.client.filter.InitLoginUserFilter" init-method="initConfig"/>
cas_config.properties代码
service = http://127.0.0.1:8090/webapp/main.do
casServerUrlPrefix = http://127.0.0.1:8081/cas-server/
casServerLoginUrl = http://127.0.0.1:8081/cas-server/login
casServerLogoutUrl = http://127.0.0.1:8081/cas-server/logout
errorUrl = http://127.0.0.1:8081/cas-server/error
首先,从web.xml配置信息可以看出,有三个过滤器负责处理单点登录流程,其中前两个为流程必须处理的过滤器,最后一个是我新增加的过滤器,主要功能是初始化用户信息和权限。既然是源码解读,我们必然要看源码。下面就从CAS客户端的源码给大家进行解读CAS单点登录客户端的工作流程。之后会单独写一篇关于CAS单点登录服务器的源码解读的文章。
AuthenticationFilter
从单点登录第一个过滤器开始解读,源码中的第一个过滤器是AuthenticationFilter,因为业务需求对此过滤器进行了二次开发。源码是从web.xml配置文件中读取初始化参数,我对此做了修改,从properties文件中读取初始化参数。把单点登录的配置参数定义到properties文件中,这样做的好处是对初始化配置参数统一管理,一目了然。源码走起。
AuthenticationFilter的父类AbstractCasFilter的初始化方法,主要工作是从properties中获取单点登录的系统参数。使大家看的更清楚,源码中省略了日志的输出。
public void initConfig() throws ServletException {
Properties conf = PropertiesUtil.getConfigProperties();
setServerName(conf.getProperty("serverName", null));
setService(conf.getProperty("service", null));
setArtifactParameterName(conf.getProperty("artifactParameterName", "ticket"));
setServiceParameterName(conf.getProperty("serviceParameterName", "service"));
setEncodeServiceUrl(parseBoolean(conf.getProperty("encodeServiceUrl", "true")));
//登录错误跳转到该url地址
setErrorUrl(conf.getProperty("errorUrl", null));
//初始化配置文件(AuthenticationFilter类)
initInternalConfig(conf);
init();//初始化
}
AuthenticationFilter的初始化方法。主要工作是从properties中获取单点登录的系统参数。使大家看的更清楚,源码中省略了日志的输出。
public void initInternalConfig(Properties conf) throws ServletException {
if (!isIgnoreInitConfiguration()) {
setCasServerLoginUrl(conf.getProperty("casServerLoginUrl", null));
setRenew(parseBoolean(conf.getProperty("renew", "false")));
setGateway(parseBoolean(conf.getProperty("gateway", "false")));
String gatewayStorageClass = conf.getProperty("gatewayStorageClass", null);
if (gatewayStorageClass == null)
return;
try {
this.gatewayStorage = ((GatewayResolver) Class.forName(gatewayStorageClass).newInstance());
} catch (Exception e) {
this.log.error(e, e);
throw new ServletException(e);
}
}
}
AuthenticationFilter的过滤方法。
public final void doFilter(final ServletRequest servletRequest,
final ServletResponse servletResponse, final FilterChain filterChain)
throws IOException, ServletException {
final HttpServletRequest request = (HttpServletRequest) servletRequest;
final HttpServletResponse response = (HttpServletResponse) servletResponse;
final HttpSession session = request.getSession(false);
//从session中获取名为"_const_cas_assertion_"的Assertion
final Assertion assertion = (session != null) ? (Assertion) session.getAttribute(CONST_CAS_ASSERTION) : null;
//如果存在,则说明已经登录,本过滤器处理完成,处理下个过滤器
if (assertion != null) {
filterChain.doFilter(request, response);
return;
}
//生成serviceUrl
final String serviceUrl = constructServiceUrl(request, response);
//从request中获取参数ticket(ST-1-4hH2s5tzsMGCcToDvGCb-cas01.example.org)
final String ticket = CommonUtils.safeGetParameter(request, getArtifactParameterName());
final boolean wasGatewayed = this.gatewayStorage.hasGatewayedAlready(request, serviceUrl);
//如果ticket不为空,本过滤器处理完成,处理下个过滤器
if ((CommonUtils.isNotBlank(ticket)) || (wasGatewayed)) {
filterChain.doFilter(request, response);
return;
}
this.log.debug("no ticket and no assertion found");
final String modifiedServiceUrl;
if (this.gateway) {
this.log.debug("setting gateway attribute in session");
modifiedServiceUrl = this.gatewayStorage.storeGatewayInformation(request, serviceUrl);
} else {
modifiedServiceUrl = serviceUrl;
}
if (this.log.isDebugEnabled()) {
this.log.debug("Constructed service url: " + modifiedServiceUrl);
}
//生成重定向URL
final String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl, getServiceParameterName(),
modifiedServiceUrl, this.renew, this.gateway);
if (this.log.isDebugEnabled()) {
this.log.debug("redirecting to \"" + urlToRedirectTo + "\"");
}
//跳转到CAS服务器的登录页面
response.sendRedirect(urlToRedirectTo);
}
当我们从浏览器访问配置了单点登录的应用系统时(http://127.0.0.1:8090/webapp/main.do),由于集成了CAS单点登录客户端,此时进入到第一个过滤器AuthenticationFilter(不考虑其他非单点登录的过滤器),执行以下操作:
- 从session中获取名为“_const_cas_assertion_”的assertion对象,判断assertion是否存在,如果存在,说明已经登录,执行下一个过滤器。如果不存在,执行第2步。
- 生成serviceUrl(http://127.0.0.1:8090/webapp/main.do),从request中获取票据参数ticket,判断ticket是否为空,如果不为空执行下一个过滤器。如果为空,执行第3步。
- 生成重定向URL(http://127.0.0.1:8081/cas-server/login?service=http://127.0.0.1:8090/webapp/main.do)。
- 跳转到单点登录服务器,显示登录页面,此时第一个过滤器执行完成。
TicketValidationFilter
AbstractTicketValidationFilter的过滤方法。
public final void doFilter(final ServletRequest servletRequest,
final ServletResponse servletResponse, final FilterChain filterChain)
throws IOException, ServletException {
if (!preFilter(servletRequest, servletResponse, filterChain)) {
return;
}
final HttpServletRequest request = (HttpServletRequest) servletRequest;
final HttpServletResponse response = (HttpServletResponse) servletResponse;
//从request中获取参数
final String ticket = CommonUtils.safeGetParameter(request, getArtifactParameterName());
//ticket不为空,验证ticket,否则本过滤器处理完成,处理下个过滤器
if (CommonUtils.isNotBlank(ticket)) {
if (this.log.isDebugEnabled()) {
this.log.debug("Attempting to validate ticket: " + ticket);
}
try {
//验证ticket并产生Assertion对象,错误抛出TicketValidationException异常
final Assertion assertion = this.ticketValidator.validate(ticket, constructServiceUrl(request, response));
if (this.log.isDebugEnabled()) {
this.log.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);
}
onSuccessfulValidation(request, response, assertion);
if (this.redirectAfterValidation) {
this.log.debug("Redirecting after successful ticket validation.");
response.sendRedirect(constructServiceUrl(request, response));
return;
}
} catch (final TicketValidationException e) {
response.setStatus(403);
this.log.info("获得用户认证信息失败:" + e.getMessage());
//跳转到错误页面
response.sendRedirect(getErrorUrl() + "?errortype=" + e.getMessage() + "&" + createService());
return;
}
}
//本过滤器处理完成,处理下个过滤器
filterChain.doFilter(request, response);
}
假设当执行完第一个过滤器后,跳转到CAS服务器端的登录页面,输入用户名和密码,验证通过后。CAS服务器端会生成ticket,并将ticket作为重新跳转到应用系统的参数(http://127.0.0.1:8090/webapp/main.do?ticket=ST-1-4hH2s5tzsMGCcToDvGCb-cas01.example.org)。此时又进入第一个过滤器AuthenticationFilter,由于存在ticket参数,进入到第二个过滤器TicketValidationFilter,执行以下操作:
- 从request获取ticket参数,如果ticket为空,继续处理下一个过滤器。如果参数不为空,验证ticket参数的合法性。
- 验证ticket,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/webapp/main.do)验证ticket是否正确,并返回assertion对象。如果验证失败,抛出异常,跳转到错误页面。如果验证成功,session会以"_const_cas_assertion_"的名称保存assertion对象,继续处理下一个过滤器。
InitLoginUserFilter
public final void doFilter(final ServletRequest servletRequest,
final ServletResponse servletResponse, final FilterChain filterChain)
throws IOException, ServletException {
try {
//加上自己应用的逻辑,例如构造用户的信息和权限等
//...
//本过滤器处理完成,处理下个过滤器
filterChain.doFilter(request, response);
return;
} catch (Exception e) {
//跳转到错误页面
res.sendRedirect(getErrorUrl() + "?errortype=" + e.getMessage() + "&" + createService());
}
}
这个过滤器是我新增加的过滤器,以满足应用系统的特殊业务。当到达这个过滤器的时候,说明CAS服务器已经登录成功,此过滤器会对应用系统所需要的信息进行初始化(用户基本信息和权限等)。完成操作后,显示应用系统的首页。如果出现异常,跳转到错误页面。