cas SSO單點登入相關內容
小記:
在做一套系統,準備接入整合登入。但是該系統對接的使用者系統過多,所以要給每個使用者系統在web.xml中配置相應的過濾器,導致web.xml過於冗雜龐大,不利於管理。解決方案:給所有使用者的登入請求配置成同一個登入請求,在web.xml裡配置一套統一的過濾器,不配置init-param初始化引數,將初始化引數配置到使用者各自的properties檔案中,
例:casServerUrlPrefix=cas服務認證ip或域名
serverName=客戶端ip或域名
encoding=UTF-8
casServerLoginUrl=
然後閱讀原始碼改寫其獲取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);
}
}