1. 程式人生 > >cas SSO單點登入相關內容

cas SSO單點登入相關內容

小記:

        在做一套系統,準備接入整合登入。但是該系統對接的使用者系統過多,所以要給每個使用者系統在web.xml中配置相應的過濾器,導致web.xml過於冗雜龐大,不利於管理。解決方案:給所有使用者的登入請求配置成同一個登入請求,在web.xml裡配置一套統一的過濾器,不配置init-param初始化引數,將初始化引數配置到使用者各自的properties檔案中,

例:casServerUrlPrefix=cas服務認證ip或域名

serverName=客戶端ip或域名

encoding=UTF-8

casServerLoginUrl=

cas服務認證ip或域名/login

然後閱讀原始碼改寫其獲取init-param的方法,增加從properties讀取檔案的方式。以下是基於cas-client-core-3.2.1.jar包的獲取初始化引數的原始碼

.

protected final String getPropertyFromInitParams(FilterConfig filterConfig,	String propertyName, String defaultValue) {
        // 此處是從properties獲取初始化引數
		Properties prop = FileTools.readProperties("DBConfig_"	+ Formater.ntrim(FileTools.readProperties("SystemConfig.properties").getProperty("UnitCode")) + ".properties");
		String value0 = prop.getProperty(propertyName);
		if (CommonUtils.isNotBlank(value0)) {
			log.info((new StringBuilder()).append("Property [").append(propertyName).append("] loaded from Properties.getProperty with value [").append(value0).append("]").toString());
			return value0;
		}
        // 此處是從filterConfig獲取初始化引數
		String value1 = filterConfig.getInitParameter(propertyName);
		if (CommonUtils.isNotBlank(value1)) {
			log.info((new StringBuilder()).append("Property [").append(propertyName).append("] loaded from FilterConfig.getInitParameter with value [").append(value1).append("]").toString());
			return value1;
		}
        // 此處是從ServletContext獲取初始化引數
		String value2 = filterConfig.getServletContext().getInitParameter(
				propertyName);
		if (CommonUtils.isNotBlank(value2)) {
			log.info((new StringBuilder()).append("Property [").append(propertyName).append("] loaded from ServletContext.getInitParameter with value [").append(value2).append("]").toString());
			return value2;
		}
		InitialContext context;
		try {
			context = new InitialContext();
		} catch (NamingException e) {
			log.warn(e, e);
			return defaultValue;
		}
		String shortName = getClass().getName().substring(
				getClass().getName().lastIndexOf(".") + 1);
		String value3 = loadFromContext(
				context,
				(new StringBuilder()).append("java:comp/env/cas/")
						.append(shortName).append("/").append(propertyName)
						.toString());
		if (CommonUtils.isNotBlank(value3)) {
			log.info((new StringBuilder())
					.append("Property [")
					.append(propertyName)
					.append("] loaded from JNDI Filter Specific Property with value [")
					.append(value3).append("]").toString());
			return value3;
		}
		String value4 = loadFromContext(
				context,
				(new StringBuilder()).append("java:comp/env/cas/")
						.append(propertyName).toString());
		if (CommonUtils.isNotBlank(value4)) {
			log.info((new StringBuilder()).append("Property [")
					.append(propertyName)
					.append("] loaded from JNDI with value [").append(value4)
					.append("]").toString());
			return value4;
		} else {
			log.info((new StringBuilder()).append("Property [")
					.append(propertyName)
					.append("] not found.  Using default value [")
					.append(defaultValue).append("]").toString());
			return defaultValue;
		}
	}

第一次看原始碼的心得:難看懂的不是程式碼,在熟悉理解底層機制後,是很容易看懂原始碼的,因為並不需要每個方法都去理解,順著流程走就行。例如明白filter的工作機制後,就直接去找其初始化方法init(FilterConfig filterConfig),裡面呼叫了getPropertyFromInitParams獲取初始化引數,然後就新增從properties讀取引數的邏輯就可以了。

 

        因為需求原因,刪掉了在web.xml裡配置filter,直接將初始化操作和過濾操作寫進了登入邏輯servlet的doPost方法裡

以下是doPost方法的初始化工作和過濾操作。

public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    Cas20ProxyReceivingTicketValidationFilter ticketValidation = 
        new Cas20ProxyReceivingTicketValidationFilter();// 校驗ticket的物件
    AuthenticationFilter authentication = new AuthenticationFilter();// 伺服器認證物件
    HttpServletRequestWrapperFilter requestWrapper = new HttpServletRequestWrapperFilter();// 儲存登入使用者物件
    ticketValidation.init(1);	// 校驗ticket物件的初始化
    authentication.init(1);		// 伺服器認證物件的初始化
    requestWrapper.init(1);		// 儲存登入使用者物件的初始化
    if (ticketValidation.doFilter(request, response, 1))
    {
        if (authentication.doFilter(request, response, 1)){
            requestWrapper.doFilter(request, response, 1);
    }else{
        return;
        }
    }else{
        return;
    }
}

        改寫了init方法的引數型別,因為用不到FilterConfig類了,也刪掉了繼承Filter類。doFilter方法也只是一個方法名,並不是過濾器的doFilter方法,這裡懶得改名字就沿用了原方法名doFilter。以下是三個doFilter方法。修改了doFilter返回值為boolean,用來確定是否繼續執行該servlet,模仿了過濾器的過濾功能,如果ticket校驗結果為true則繼續執行伺服器認證,伺服器認證結果為true則將登陸資訊載入到request中。過濾器的鏈式過濾chan.doFilter的實現

ticket校驗如下:ticketValidation.doFilter()

public class AuthenticationFilter extends AbstractCasFilter {
	
	private String casServerLoginUrl;	//sso中心認證服務的登入地址。
	private boolean renew = false;
	private boolean gateway = false;	//閘道器設定,為true能跳過登陸?
	private GatewayResolver gatewayStorage = new DefaultGatewayResolverImpl();	//閘道器儲存解析器。
	protected void initInternal(int filterConfig) throws ServletException {
		if (!isIgnoreInitConfiguration()) {
			super.initInternal(filterConfig);
			setCasServerLoginUrl(getPropertyFromInitParams(filterConfig, "casServerLoginUrl", null));
			this.log.trace("Loaded CasServerLoginUrl parameter: " + this.casServerLoginUrl);
			setRenew(parseBoolean(getPropertyFromInitParams(filterConfig, "renew", "false")));
			this.log.trace("Loaded renew parameter: " + this.renew);
			setGateway(parseBoolean(getPropertyFromInitParams(filterConfig, "gateway", "false")));
			this.log.trace("Loaded gateway parameter: " + this.gateway);
			String gatewayStorageClass = getPropertyFromInitParams(
					filterConfig, "gatewayStorageClass", null);
			if (gatewayStorageClass != null) {
				try {
					this.gatewayStorage = ((GatewayResolver) Class.forName(gatewayStorageClass).newInstance());
				} catch (Exception e) {
					this.log.error(e, e);
					throw new ServletException(e);
				}
			}
		}
	}
	public void init() {
		super.init();
		CommonUtils.assertNotNull(this.casServerLoginUrl, "casServerLoginUrl cannot be null.");
	}
	public final boolean doFilter(ServletRequest servletRequest, ServletResponse servletResponse, int filterChain) throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) servletRequest;
		HttpServletResponse response = (HttpServletResponse) servletResponse;
		//獲取sso認證中心儲存的session屬性_const_cas_assertion_
		HttpSession session = request.getSession(false);
		// 該變數為判斷使用者是否已經登入的標記,在ticket認證成功後會被設定
		Assertion assertion = session != null ? (Assertion) session.getAttribute("_const_cas_assertion_") : null;
		// 如果登入過,則直接認證通過
		if (assertion != null) {
			return true;
		}
		// 從request中構建需要認證的服務url。如果該Url包含tikicet引數,則去除引數--http://218.242.158.194:18888/LoginByCQJTU去掉了後面的ticket=STXXX-XXX
		String serviceUrl = constructServiceUrl(request, response);
		// 從request中獲取票據ticket。如果ticket存在,則獲取URL後面的引數ticket
		String ticket = CommonUtils.safeGetParameter(request, getArtifactParameterName());
		// 如果存在閘道器設定,則從session當中獲取屬性_const_cas_gateway的值為閘道器設定,並從session中去掉此屬性。
		boolean wasGatewayed = this.gatewayStorage.hasGatewayedAlready(request, serviceUrl);
		// 如果存在認證票據ticket或者閘道器設定,則直接認證通過。
		if ((CommonUtils.isNotBlank(ticket)) || (wasGatewayed)) {
			return true;
		}
		// 未登入,ticket不存在
		this.log.debug("no ticket and no assertion found");
		String modifiedServiceUrl;
		if (this.gateway) {
			this.log.debug("setting gateway attribute in session");
			//在session中設定閘道器屬性session.setAttribute("_const_cas_gateway_", "yes")
			modifiedServiceUrl = this.gatewayStorage.storeGatewayInformation(request, serviceUrl);
		} else {
			modifiedServiceUrl = serviceUrl;
		}
		if (this.log.isDebugEnabled()) {
			this.log.debug("Constructed service url: " + modifiedServiceUrl);
		}
		// 如果使用者沒有登入過,那麼構造重定向的URL
		String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl,
				getServiceParameterName(), modifiedServiceUrl, 
				this.renew, this.gateway);
		if (this.log.isDebugEnabled()) {
			this.log.debug("redirecting to \"" + urlToRedirectTo + "\"");
		}
		// 重定向跳轉到Cas認證中心,不走servlet路徑,等待登入結果
		response.sendRedirect(urlToRedirectTo);
		return false;
	}
	public final void setRenew(boolean renew) {
		this.renew = renew;
	}
	public final void setGateway(boolean gateway) {
		this.gateway = gateway;
	}
	public final void setCasServerLoginUrl(String casServerLoginUrl) {
		this.casServerLoginUrl = casServerLoginUrl;
	}
	public final void setGatewayStorage(GatewayResolver gatewayStorage) {
		this.gatewayStorage = gatewayStorage;
	}
}

伺服器端認證如下:authentication.doFilter()

/**
 * 伺服器端認證
 * 第一次進入重定向到cas server,進入登入介面。使用者資訊認證通過後,建立了新的TGT後,快取TGT,並且生成cookie,待後續把cookie寫入客戶端
 * 		然後驗證是否存在Service,如果存在,生成ST,重定向使用者到 Service 所在地址(附帶該ST,並且會被過濾器攔截) , 併為客戶端瀏覽器設定一個 Ticket Granted Cookie ( TGC )
 * 第二次得到ticket後會執行doFilter進行ticket驗證,驗證成功會設定assertion,並再次重定向到Service執行攔截器邏輯,assertion認證成功。
 * 
 * 登入後再次訪問,則直接assertion認證成功,繼續執行doFilter。
 */
public class AuthenticationFilter extends AbstractCasFilter {
	
	private String casServerLoginUrl;	//sso中心認證服務的登入地址。
	private boolean renew = false;
	private boolean gateway = false;	//閘道器設定?
	private GatewayResolver gatewayStorage = new DefaultGatewayResolverImpl();	//閘道器儲存解析器。
	protected void initInternal(int filterConfig) throws ServletException {
		if (!isIgnoreInitConfiguration()) {
			super.initInternal(filterConfig);
			setCasServerLoginUrl(getPropertyFromInitParams(filterConfig, "casServerLoginUrl", null));
			this.log.trace("Loaded CasServerLoginUrl parameter: " + this.casServerLoginUrl);
			setRenew(parseBoolean(getPropertyFromInitParams(filterConfig, "renew", "false")));
			this.log.trace("Loaded renew parameter: " + this.renew);
			setGateway(parseBoolean(getPropertyFromInitParams(filterConfig, "gateway", "false")));
			this.log.trace("Loaded gateway parameter: " + this.gateway);
			String gatewayStorageClass = getPropertyFromInitParams(
					filterConfig, "gatewayStorageClass", null);
			if (gatewayStorageClass != null) {
				try {
					this.gatewayStorage = ((GatewayResolver) Class.forName(gatewayStorageClass).newInstance());
				} catch (Exception e) {
					this.log.error(e, e);
					throw new ServletException(e);
				}
			}
		}
	}
	public void init() {
		super.init();
		CommonUtils.assertNotNull(this.casServerLoginUrl, "casServerLoginUrl cannot be null.");
	}
	public final boolean doFilter(ServletRequest servletRequest, ServletResponse servletResponse, int filterChain) throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) servletRequest;
		HttpServletResponse response = (HttpServletResponse) servletResponse;
		//獲取sso認證中心儲存的session屬性_const_cas_assertion_
		HttpSession session = request.getSession(false);
		// 該變數為判斷使用者是否已經登入的標記,在ticket認證成功後會被設定
		Assertion assertion = session != null ? (Assertion) session.getAttribute("_const_cas_assertion_") : null;
		// 如果登入過,則直接認證通過
		if (assertion != null) {
			return true;
		}
		// 從request中構建需要認證的服務url。如果該Url包含tikicet引數,則去除引數--http://218.242.158.194:18888/LoginByCQJTU去掉了後面的ticket=STXXX-XXX
		String serviceUrl = constructServiceUrl(request, response);
		// 從request中獲取票據ticket。如果ticket存在,則獲取URL後面的引數ticket
		String ticket = CommonUtils.safeGetParameter(request, getArtifactParameterName());
		// 如果存在閘道器設定,則從session當中獲取屬性_const_cas_gateway的值為閘道器設定,並從session中去掉此屬性。
		boolean wasGatewayed = this.gatewayStorage.hasGatewayedAlready(request, serviceUrl);
		// 如果存在認證票據ticket或者閘道器設定,則直接認證通過。
		if ((CommonUtils.isNotBlank(ticket)) || (wasGatewayed)) {
			return true;
		}
		// 未登入,ticket不存在
		this.log.debug("no ticket and no assertion found");
		String modifiedServiceUrl;
		if (this.gateway) {
			this.log.debug("setting gateway attribute in session");
			//在session中設定閘道器屬性session.setAttribute("_const_cas_gateway_", "yes")
			modifiedServiceUrl = this.gatewayStorage.storeGatewayInformation(request, serviceUrl);
		} else {
			modifiedServiceUrl = serviceUrl;
		}
		if (this.log.isDebugEnabled()) {
			this.log.debug("Constructed service url: " + modifiedServiceUrl);
		}
		// 如果使用者沒有登入過,那麼構造重定向的URL
		String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl,
				getServiceParameterName(), modifiedServiceUrl, 
				this.renew, this.gateway);
		if (this.log.isDebugEnabled()) {
			this.log.debug("redirecting to \"" + urlToRedirectTo + "\"");
		}
		// 重定向跳轉到Cas認證中心,不走servlet路徑,等待登入結果
		response.sendRedirect(urlToRedirectTo);
		return false;
	}
	public final void setRenew(boolean renew) {
		this.renew = renew;
	}
	public final void setGateway(boolean gateway) {
		this.gateway = gateway;
	}
	public final void setCasServerLoginUrl(String casServerLoginUrl) {
		this.casServerLoginUrl = casServerLoginUrl;
	}
	public final void setGatewayStorage(GatewayResolver gatewayStorage) {
		this.gatewayStorage = gatewayStorage;
	}
}

驗證通過,設定使用者資訊。requestWrapper.doFilter(),其中修改了設定principal的方式,獲取的時候不通過getUserPrincipal()和getRemoteUser(),直接通過request.getAttribute("principal")獲得principal物件,然後principal.getName()就能獲得使用者唯一標時。

public final class HttpServletRequestWrapperFilter extends AbstractConfigurationFilter {
	private String roleAttribute;
	private boolean ignoreCase;
	public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, int filterChain) throws IOException, ServletException {
		AttributePrincipal principal = retrievePrincipalFromSessionOrRequest(servletRequest);
		servletRequest.setAttribute("principal", principal);
		//ServletRequest servletrequest = new CasHttpServletRequestWrapper((HttpServletRequest) servletRequest, principal);
		//filterChain.doFilter(servletrequest, servletResponse);
	}
}