單點登陸解決方案 —— Smart SSO

前幾天我把 CAS 稍微研究了一下,感受這個東西還有有點意思的,因此打算把它集成到 Smart 框架中來,但又不想與 Smart 耦合地太緊,因而我單獨作了一個項目,叫作 Smart SSO。java

Smart SSO 實際上與 Smart Framework 沒有任何的耦合,但能夠集成到 Smart 應用中,固然也能夠集成到沒有使用 Smart 框架的應用中,是否是有點意思?git

下面我就與你們分享一下個人解決方案吧!web


若是您還不瞭解 SSO 或 CAS,建議先閱讀我寫的這兩篇博文:shell


第一步:搭建一個 Smart SSO 的 Maven 項目

在 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。服務器


第二步:定義一個 Web 應用初始化接口

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 吧。


第三步:使用 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,支持開源中國!

相關文章
相關標籤/搜索