前幾天我把 CAS 稍微研究了一下,感受這個東西還有有點意思的,因此打算把它集成到 Smart 框架中來,但又不想與 Smart 耦合地太緊,因而我單獨作了一個項目,叫作 Smart SSO。java
Smart SSO 實際上與 Smart Framework 沒有任何的耦合,但能夠集成到 Smart 應用中,固然也能夠集成到沒有使用 Smart 框架的應用中,是否是有點意思?git
下面我就與你們分享一下個人解決方案吧!web
若是您還不瞭解 SSO 或 CAS,建議先閱讀我寫的這兩篇博文:shell
安裝 CAS 服務器:http://my.oschina.net/huangyong/blog/198109apache
原來能夠這樣玩 SSO:http://my.oschina.net/huangyong/blog/198519
編程
在 pom.xml 中編寫如下配置:
api
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.smart</groupId> <artifactId>smart-parent</artifactId> <version>1.0</version> <relativePath>../smart-parent/pom.xml</relativePath> </parent> <artifactId>smart-sso</artifactId> <version>1.0</version> <dependencies> <!-- JUnit --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> </dependency> <!-- SLF4J --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> </dependency> <!-- Servlet --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> </dependency> <!-- CAS --> <dependency> <groupId>org.jasig.cas.client</groupId> <artifactId>cas-client-core</artifactId> </dependency> </dependencies> </project>
可見,這個項目沒有對 Smart Framework 及其 Plugin 有任何依賴,但它必須依賴 CAS Client 與 Servlet API。服務器
import javax.servlet.ServletContext; public interface WebApplicationInitializer { void init(ServletContext servletContext); }
很顯然,這裏的 init 方法是用來初始化的,我想讓 Web 應用被 Web 容器加載的時候就能初始化,如何實現呢?session
方案有兩種:app
方案一:寫一個類,讓它實現 javax.servlet.ServletContextListener 接口(它是一個 Listener,就像 Smart Framework 中的 ContainerListener 那樣)。
方案二:寫一個類,讓它實現 javax.servlet.ServletContainerInitializer 接口(它是 Servlet 3.0 提供的特性)。
選擇哪一種方式其實均可以,關鍵取決於實際狀況。
咱們打算這樣用 Smart SSO,將它打成 jar 包(smart-sso.jar),而後扔到 lib 目錄下,讓應用跑起來的時候自動加載,對於這種狀況,咱們優先使用優先使用「方案二」,緣由很簡單,由於咱們不須要定義那麼多的 ServletContextListener。
看到這裏,你必定會問:爲何要搞一個初始化接口出來?這到底是要初始化什麼?
由於 CAS Client 官方文檔告訴咱們,想要在本身的應用中加載 CAS Client,必須在 web.xml 中作以下配置:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0"> <listener> <listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class> </listener> <filter> <filter-name>SingleSignOutFilter</filter-name> <filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class> </filter> <filter-mapping> <filter-name>SingleSignOutFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <filter> <filter-name>AuthenticationFilter</filter-name> <filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class> <init-param> <param-name>casServerLoginUrl</param-name> <param-value>https://cas:8443/login</param-value> </init-param> <init-param> <param-name>serverName</param-name> <param-value>http://server:8080</param-value> </init-param> </filter> <filter-mapping> <filter-name>AuthenticationFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <filter> <filter-name>TicketValidationFilter</filter-name> <filter-class>org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter</filter-class> <init-param> <param-name>casServerUrlPrefix</param-name> <param-value>https://cas:8443</param-value> </init-param> <init-param> <param-name>serverName</param-name> <param-value>http://server:8080</param-value> </init-param> </filter> <filter-mapping> <filter-name>TicketValidationFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <filter> <filter-name>RequestWrapperFilter</filter-name> <filter-class>org.jasig.cas.client.util.HttpServletRequestWrapperFilter</filter-class> </filter> <filter-mapping> <filter-name>RequestWrapperFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <filter> <filter-name>AssertionThreadLocalFilter</filter-name> <filter-class>org.jasig.cas.client.util.AssertionThreadLocalFilter</filter-class> </filter> <filter-mapping> <filter-name>AssertionThreadLocalFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> </web-app>
可參考 CAS Client 的官方文檔:
https://wiki.jasig.org/display/CASC/Configuring+the+Jasig+CAS+Client+for+Java+in+the+web.xml
固然 CAS 也能夠與 Spring 集成,可是仍是少不了你在 web.xml 中配置,能夠參考這篇官方文檔:
https://wiki.jasig.org/display/CASC/Configuring+the+JA-SIG+CAS+Client+for+Java+using+Spring
可見,在 web.xml 中定義來一大堆的 Filter,還有一個 Listener。這些配置確實又臭又長,實在有些受不了,要是能在 config.properties 裏像這樣配置就行了:
sso=true sso.app_url=http://server:8080 sso.cas_url=https://cas:8443 sso.filter_mapping=/*
爲了實現這個特性(讓 web.xml 零配置),咱們可以使用 Servlet 3.0 的 API 來經過編程的方式來註冊這些 Filter 與 Listener(固然也能夠是 Servlet)。
如何作到這一切呢?咱們不妨先來實現這個 WebApplicationInitializer 吧。
實現 WebApplicationInitializer 接口其實是一件十分簡單的事情,咱們只須要了解一下 ServletContext 的 API 便可。
無非就是調用它的 addFilter 與 addListener 方法,把 CAS 的 Filter 與 Listener 註冊到 ServletContext 中,這樣就不須要在 web.xml 中配置了(Spring 3.0 也是這樣玩的)。
下面咱們不妨定義一個 SmartWebApplicationInitializer 類吧,讓它去實現 WebApplicationInitializer 接口,代碼以下:
import javax.servlet.FilterRegistration; import javax.servlet.ServletContext; import org.jasig.cas.client.authentication.AuthenticationFilter; import org.jasig.cas.client.session.SingleSignOutFilter; import org.jasig.cas.client.session.SingleSignOutHttpSessionListener; import org.jasig.cas.client.util.AssertionThreadLocalFilter; import org.jasig.cas.client.util.HttpServletRequestWrapperFilter; import org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter; public class SmartWebApplicationInitializer implements WebApplicationInitializer { @Override public void init(ServletContext servletContext) { if (ConfigProps.isSSO()) { String casServerUrlPrefix = ConfigProps.getCasServerUrlPrefix(); String casServerLoginUrl = ConfigProps.getCasServerLoginUrl(); String serverName = ConfigProps.getServerName(); String filterMapping = ConfigProps.getFilterMapping(); servletContext.addListener(SingleSignOutHttpSessionListener.class); FilterRegistration.Dynamic singleSignOutFilter = servletContext.addFilter("SingleSignOutFilter", SingleSignOutFilter.class); singleSignOutFilter.addMappingForUrlPatterns(null, false, filterMapping); FilterRegistration.Dynamic authenticationFilter = servletContext.addFilter("AuthenticationFilter", AuthenticationFilter.class); authenticationFilter.setInitParameter("casServerLoginUrl", casServerLoginUrl); authenticationFilter.setInitParameter("serverName", serverName); authenticationFilter.addMappingForUrlPatterns(null, false, filterMapping); FilterRegistration.Dynamic ticketValidationFilter = servletContext.addFilter("TicketValidationFilter", Cas20ProxyReceivingTicketValidationFilter.class); ticketValidationFilter.setInitParameter("casServerUrlPrefix", casServerUrlPrefix); ticketValidationFilter.setInitParameter("serverName", ConfigProps.getServerName()); ticketValidationFilter.addMappingForUrlPatterns(null, false, filterMapping); FilterRegistration.Dynamic requestWrapperFilter = servletContext.addFilter("RequestWrapperFilter", HttpServletRequestWrapperFilter.class); requestWrapperFilter.addMappingForUrlPatterns(null, false, filterMapping); FilterRegistration.Dynamic assertionThreadLocalFilter = servletContext.addFilter("AssertionThreadLocalFilter", AssertionThreadLocalFilter.class); assertionThreadLocalFilter.addMappingForUrlPatterns(null, false, filterMapping); } } }
咱們先經過 ConfigProps 類的靜態方法從 config.properties 文件中獲取相關的配置項,而後調用 Servlet API 進行註冊,以上代碼想必已經很是清楚了。
那麼 ConfigProps 的代碼是怎樣的呢?其實這裏沒有用 Smart Framework 的 ConfigHelper,儘管它已經很是好用了,爲了避免與它發生耦合,咱們只需簡單地編寫一個 properties 文件讀取類就能夠了,代碼以下:
import java.io.IOException; import java.io.InputStream; import java.util.Properties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class ConfigProps { private static final Logger logger = LoggerFactory.getLogger(ConfigProps.class); private static final Properties configProps = new Properties(); static { InputStream is = null; try { is = Thread.currentThread().getContextClassLoader().getResourceAsStream("config.properties"); configProps.load(is); } catch (IOException e) { logger.error("加載屬性文件出錯!", e); } finally { if (is != null) { try { is.close(); } catch (IOException e) { logger.error("釋放資源出錯!", e); } } } } public static boolean isSSO() { return Boolean.parseBoolean(configProps.getProperty("sso")); } public static String getCasServerUrlPrefix() { return configProps.getProperty("sso.cas_url"); } public static String getCasServerLoginUrl() { return configProps.getProperty("sso.cas_url") + "/login"; } public static String getServerName() { return configProps.getProperty("sso.app_url"); } public static String getFilterMapping() { return configProps.getProperty("sso.filter_mapping"); } }
上面的步驟中,咱們編寫了自定義的 WebApplicationInitializer 接口,並對其作了一個實現。
那麼這個 WebApplicationInitializer 接口又是如何被 Web 容器發現並調用的呢?神奇的事情即將發生!
沒錯,咱們只需實現 ServletContainerInitializer 接口,而且在 META-INF 中添加一個 services 目錄,在該目錄中添加一個 javax.servlet.ServletContainerInitializer 文件便可,你沒有看錯,文件名就是一個這個接口的徹底名稱。注意,不是 WEB-INF,而是 META-INF,咱們能夠將其放在 Maven 的 resources 目錄下,與 Java 的 classpath 在同一級。
那麼 ServletContainerInitializer 又是如何知道 WebApplicationInitializer 的呢?
咱們須要藉助 Servlet 3.0 的 javax.servlet.annotation.HandlesTypes 註解來實現,代碼以下:
import java.util.Set; import javax.servlet.ServletContainerInitializer; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.annotation.HandlesTypes; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @HandlesTypes(WebApplicationInitializer.class) public class SmartServletContainerInitializer implements ServletContainerInitializer { private static final Logger logger = LoggerFactory.getLogger(SmartServletContainerInitializer.class); @Override public void onStartup(Set<Class<?>> webApplicationInitializerClassSet, ServletContext servletContext) throws ServletException { try { for (Class<?> webApplicationInitializerClass : webApplicationInitializerClassSet) { WebApplicationInitializer webApplicationInitializer = (WebApplicationInitializer) webApplicationInitializerClass.newInstance(); webApplicationInitializer.init(servletContext); } } catch (Exception e) { logger.error("初始化出錯!", e); } } }
首先在 SmartServletContainerInitializer 類上標註了 @HandlesTypes 註解,讓它去加載 WebApplicationInitializer 類。注意,在該註解中必定要用接口,不能用實現類。
當實現了 ServletContainerInitializer 接口後,咱們必須實現該接口的 onStartup 方法,在該方法中可獲取實現了 WebApplicationInitializer 接口的全部實現類(其實只有一個實現類),循環它們,並經過反射建立對應的實例。最後經過多態的方式調用接口的 init 方法,將 ServletContext 傳入便可。
那麼,META-INF/services/javax.servlet.ServletContainerInitializer 這個文件裏到底有什麼祕密呢?
com.smart.sso.SmartServletContainerInitializer
沒什麼神奇的,裏面只有一行,就是咱們剛纔實現 ServletContainerInitializer 接口的實現類的徹底類名。
好了,Smart SSO 全部的開發過程已所有結束,就這麼簡單,剩下來的就是在你的應用中使用它了。
咱們能夠在 Maven 中添加 Smart SSO 的依賴:
... <dependency> <groupId>com.smart</groupId> <artifactId>smart-sso</artifactId> <version>1.0</version> </dependency> ...
感受如何?CAS 就這樣被整合進來了,咱們無需配置 web.xml,只需使用 Smart SSO 這個 jar 包,而後在 config.properties 文件中添加一些配置項便可。
你還等什麼呢?趕忙來試用一下吧!
Smart SSO 源碼地址:http://git.oschina.net/huangyong/smart-sso
隨時等待您的建議或意見!請您能支持 Smart,支持開源中國!