单点登录解决方案 —— Smart SSO

原创
2014/02/13 15:54
阅读数 1.8W

前几天我把 CAS 稍微研究了一下,感觉这个东西还有有点意思的,所以打算把它集成到 Smart 框架中来,但又不想与 Smart 耦合地太紧,于是我单独做了一个项目,叫做 Smart SSO。

Smart SSO 实际上与 Smart Framework 没有任何的耦合,但可以集成到 Smart 应用中,当然也可以集成到没有使用 Smart 框架的应用中,是不是有点意思?

下面我就与大家分享一下我的解决方案吧!


如果您还不了解 SSO 或 CAS,建议先阅读我写的这两篇博文:


第一步:搭建一个 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,支持开源中国!

展开阅读全文
加载中
点击加入讨论🔥(6) 发布并加入讨论🔥
6 评论
124 收藏
8
分享
返回顶部
顶部