前几天我把 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
第一步:搭建一个 Smart SSO 的 Maven 项目
在 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。
第二步:定义一个 Web 应用初始化接口
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 吧。
第三步:使用 Servlet API 注册 CAS 的 Filter 与 Listener
实现 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 接口
没错,我们只需实现 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 所有的开发过程已全部结束,就这么简单,剩下来的就是在你的应用中使用它了。
最后一步:使用 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,支持开源中国!