前几天我把 CAS 稍微研究了一下,感觉这个东西还有有点意思的,所以打算把它集成到 Smart 框架中来,但又不想与 Smart 耦合地太紧,于是我单独做了一个项目,叫做 Smart SSO。
Smart SSO 实际上与 Smart Framework 没有任何的耦合,但可以集成到 Smart 应用中,当然也可以集成到没有使用 Smart 框架的应用中,是不是有点意思?
下面我就与大家分享一下我的解决方案吧!
如果您还不了解 SSO 或 CAS,建议先阅读我写的这两篇博文:
安装 CAS 服务器:http://my.oschina.net/huangyong/blog/198109
原来可以这样玩 SSO:http://my.oschina.net/huangyong/blog/198519
在 pom.xml 中编写以下配置:
<?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 容器加载的时候就能初始化,如何实现呢?
方案有两种:
方案一:写一个类,让它实现 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,支持开源中国!