CAS单点登录源码解析之【客户端】

前期准备:

2. cas-client-3.2.1-release.zip
3.应用系统webapp(http://127.0.0.1:8090/webapp/main.do)
4.CAS单点登录服务器端(http://127.0.0.1:8081/cas-server/)
        本次讨论包括CAS单点登录客户端的部分源码,以及在此基础上进行二次开发,因此需要修改部分CAS客户端的源码,源码部分的修改在下面进行讨论。关于 CAS服务器端的源码分析,请参考另一篇文章 http://blog.csdn.net/dovejing/article/details/44523545
其中cas-client-core-3.2.1.jar为CAS客户端的核心jar包,cas-client-3.2.1-release.zip为CAS客户端的源码zip包。
web.xml部分代码
	<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(不考虑其他非单点登录的过滤器),执行以下操作:

  1. 从session中获取名为“_const_cas_assertion_”的assertion对象,判断assertion是否存在,如果存在,说明已经登录,执行下一个过滤器。如果不存在,执行第2步。
  2. 生成serviceUrl(http://127.0.0.1:8090/webapp/main.do),从request中获取票据参数ticket,判断ticket是否为空,如果不为空执行下一个过滤器。如果为空,执行第3步。
  3. 生成重定向URL(http://127.0.0.1:8081/cas-server/login?service=http://127.0.0.1:8090/webapp/main.do)。
  4. 跳转到单点登录服务器,显示登录页面,此时第一个过滤器执行完成。

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,执行以下操作:

  1. 从request获取ticket参数,如果ticket为空,继续处理下一个过滤器。如果参数不为空,验证ticket参数的合法性。
  2. 验证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

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服务器已经登录成功,此过滤器会对应用系统所需要的信息进行初始化(用户基本信息和权限等)。完成操作后,显示应用系统的首页。如果出现异常,跳转到错误页面。

  • 4
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值