65.9K
CodeProject 正在变化。 阅读更多。
Home

通过 SAML 2.0 资源共享 SSO 集成经验

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.62/5 (5投票s)

2014 年 3 月 24 日

CPOL

16分钟阅读

viewsIcon

295413

downloadIcon

396

通过 SAML 2.0 资源共享 SSO 集成经验

1. 背景

尽管单点登录 (SSO) 已经存在、被讨论和使用很长时间,但实践表明它并非总是易于实现。本文的目的是展示如何为 SAML 2.0 身份提供者 (IdP) 实现自定义的服务提供者 (SP),并利用它将 SAML SSO 集成到您的 Java Web 应用程序中。

在我们最近的一个项目中,我们必须为一所主要大学部署一个集群门户解决方案。其中一项任务是为以下系统实现 SSO:

  1. Liferay 门户
  2. 简单的自定义 Java Web 应用程序
  3. Google Apps

包括附加要求和注意事项

  1. Shibboleth 已被选定并用作 IdP。Shibboleth 是一个开源系统,完全实现了 SAML 1.0 和 SAML 2.0 协议(http://shibboleth.net/about/index.html)。
  2. SAML 2.0 被选为 SSO 集成的主要协议。
  3. Jasig CAS 已配置为 Shibboleth 身份验证提供者。

除了更灵活的身份验证用户体验和流程配置外,这种设置还使 CAS 兼容的系统能够参与 SSO,而无需进行额外实现。

LDAP 已配置为 CAS 的身份验证提供者,并为 Shibboleth 配置为属性提供者。

我们面临的挑战包括

  1. 制造商提供的 Shibboleth 文档并不总是最新和完整的。
  2. 很少有关于 Java Web 应用程序的 SAML SP 实现的全面示例。
  3. 使用与 CAS 集成的 Shibboleth 来启用 SAML 和 CAS SSO 会使 SSO 会话管理复杂化,并需要特别注意。

在解决了这些困难之后,我们忍不住与读者分享我们的经验,以便让其他开发人员更容易理解和使用 SAML 2.0。

2. 目标受众

  1. 使用 SAML 2.0 将 SSO 功能集成到其项目中的开发人员。
  2. 正在为他们的应用程序寻找使用 SAML 2.0 集成 SSO 功能的实际示例的 Java 开发人员。
  3. 希望尝试 Shibboleth 作为 SAML 身份提供者 (IdP) 的 Java 开发人员。

为了更好地理解本文,建议具备 SAML 2.0 规范的基本知识。

3. SSO 主要组件

下图展示了系统中 SSO 的通用操作。

图表显示

  1. 2 个参与 SSO 的应用程序
    1. Java Web App – 部署到 Apache Tomcat 的基于 Java Servlet 的 Web 应用程序
    2. Google Apps – Google 云服务
  2. SP Filter – 服务提供者实现。它通过 SAML 2.0 协议与 SAML Shibboleth IdP 交互
  3. Shibboleth IdP 是用于通过 SAML 1.0 和 SAML 2.0 进行身份验证和授权的应用程序。
  4. Tomcat AS 是 Java 应用程序服务器。
  5. SP Filter 和 Shibboleth IdP 之间的交互通过安全的 HTTPS 协议进行。

请注意,图表显示 Shibboleth IdP 和 Java Web 应用程序部署在不同的 Tomcat 服务器上。但是,您可以使用单个 Tomcat 实例在一个节点上部署环境。

4. 设置 Shibboleth IdP 环境

安装和配置 Shibboleth IdP

  1. http://shibboleth.net/downloads/identity-provider/latest/ 下载最新 IdP 版本并解压到任意目录;我们将使用 $shDistr 来指代该位置。
  2. 检查 JAVA_HOME 变量是否设置正确。

    运行 $shDistr/install.sh(我们假设使用的是 Linux 发行版)。

    安装程序将询问以下信息:

    • 安装路径(例如:/opt/shib
    • IdP 服务器名称(例如:idp.local.com)。

      要在本地计算机上测试多域 SSO 配置,您可以使用一个简单的技巧,修改 /etc/hosts 文件 – 只需在 /etc/hosts 文件中将 IdP 服务器域名添加到 localhost 的别名列表中:127.0.0.1 localhost idp.local.com

    • 安装过程中生成的 Java 密钥库密码(例如:12345)。

      接下来,检查安装过程是否成功完成。

      此后我们将使用以下占位符:

      • $shHome — Shibboleth 安装目录。
      • $shHost — IdP 服务器名称。
      • $shPassword — Java 密钥库 (JKS) 的密码。
  3. 现在,作为 IdP 配置的一部分,我们需要指定“属性提供者”—— IdP 内部负责检索用户属性的组件。在我们的例子中,我们只需要 IdP 共享用户登录名。

    我们在 $shHome/conf/attribute-resolver.xml 文件中的 <AttributeResolver> 元素后添加属性的描述。

    <resolver:AttributeDefinition id="transientId" xsi:type="ad:TransientId">.
    
    <resolver:AttributeDefinition xsi:type="PrincipalName"
                 xmlns="urn:mace:shibboleth:2.0:resolver:ad"  id="userLogin" >
          <resolver:AttributeEncoder xsi:type="SAML1String"
                 xmlns="urn:mace:shibboleth:2.0:attribute:encoder" name="userLogin" />
          <resolver:AttributeEncoder xsi:type="SAML2String"
                 xmlns="urn:mace:shibboleth:2.0:attribute:encoder" name="userLogin" />
    </resolver:AttributeDefinition>

    注意:您可以使用此文件指定 IdP 可能与服务提供者共享的任何附加属性;可以使用不同的数据源,例如 LDAP 或通过 JDBC 的 DBMS。属性提供者的配置在此 有更详细的描述。

  4. 我们还需要修改 $shHome/conf/attribute-filter.xml 文件,以确保 IdP 可以与我们的 SP 共享此属性。
    <afp:AttributeFilterPolicy id="releaseUserLoginToAnyone">
        <afp:PolicyRequirementRule xsi:type="basic:ANY"/>
        <afp:AttributeRule attributeID="userLogin">
            <afp:PermitValueRule xsi:type="basic:ANY"/>
        </afp:AttributeRule>
    </afp:AttributeFilterPolicy>

    注意:您可以在此处指定更复杂和精确的规则。例如,您可以指定属性仅传输给某个 SAML SP。

  5. 我们的 Shibboleth IdP 应该了解它可以交互的节点——所谓的信赖方(https://wiki.shibboleth.net/confluence/display/SHIB2/IdPUnderstandingRP)。此信息存储在 $shHome/conf/relying-party.xml 文件中。

    让我们打开文件并添加以下元素:

    <rp:RelyingParty id="sp.local.ru" provider="https://idp.local.ru/idp/shibboleth"
                                            defaultSigningCredentialRef="IdPCredential">
       <rp:ProfileConfiguration xsi:type="saml:SAML2SSOProfile"
           signResponses="never" signAssertions="never"
           encryptNameIds="never" encryptAssertions="never" />
    </rp:RelyingParty>

    请注意,SAML 规范要求所有方(如 IdP 和 SP)都由唯一的“实体 ID”标识。通常使用相应组件的域名或 URL 作为实体 ID,尽管这不是必需的。我们为 SP 使用实体 ID `sp.local.com`,为 IdP 使用实体 ID `https://idp.local.com/idp/shibboleth`。

    我们将在本地测试整个配置,因此我们还需要将 SP 域名添加到 /etc/hosts 中 localhost 的别名列表中:127.0.0.1 localhost idp.local.com sp.local.com

    ProfileConfiguration”元素声明信赖方将需要此 IdP 的 SAML 2.0 SSO 服务,并且它不会对消息和响应进行签名或加密。

  6. 在此步骤中,我们将指定 IdP 如何检索详细的 SP 信息。为此,我们将 SP 元数据提供者添加到 $shHome/conf/relying-party.xml 文件中 IdP 元数据提供者元素旁边。
    <metadata:MetadataProvider id="IdPMD" xsi:type="metadata:FilesystemMetadataProvider" .. >
    
    <metadata:MetadataProvider id="spMD" xsi:type="metadata:FilesystemMetadataProvider"
                                metadataFile="/opt/shib/metadata/saml-sp-metadata.xml"/>

    因此,我们指示 Shibboleth IdP 在 /opt/shib/metadata/saml-sp-metadata.xml 文件中查找 SP 的定义。让我们创建包含以下内容的此文件:

    <md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="sp.local.ru">
      <md:SPSSODescriptor AuthnRequestsSigned="false" ID="sp.local.ru"
         protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
        <md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
           Location="https://sp.local.ru:8443/sso/acs" index="1" isDefault="true"/>
      </md:SPSSODescriptor>
    </md:EntityDescriptor>

    这里,我们需要理解以下几点:

    • 我们的 SAML 2.0 SP 的标识是 «sp.local.ru»。
    • <md:AssertionConsumerService> 元素中指定了 shibboleth IdP 将返回 SAML 2.0 消息的地址 Location=`https://sp.local.ru:8443/sso/acs`。
    • 最后,参数 Binding=”urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST” 表明 SP 响应将通过浏览器重定向从 shibboleth IdP 发送。
  7. 现在剩下的是选择 shibboleth IdP 如何进行实际的用户身份验证——或者用 Shibboleth 的术语来说,定义“身份验证提供者”。在生产环境中,可能有各种配置,包括通过 LDAP、DBMS 甚至 CAS 进行身份验证。我们将使用已包含的远程用户身份验证机制(https://wiki.shibboleth.net/confluence/display/SHIB2/IdPAuthRemoteUser)。

    在身份验证请求期间,shibboleth IdP 将查找 REMOTE_USER 变量。如果找到该变量,shibboleth IdP 将假定用户已通过外部系统(例如,通过 Web Apache 服务器)进行身份验证。

    为了不使本文过于复杂,在本例中,我们将通过简单的 Tomcat 配置为每个请求设置 REMOTE_USER 变量。

    恭喜!Shibboleth 设置完成。

下载和配置用于 Shibboleth IdP 的 Tomcat

  1. 首先,从 https://tomcat.net.cn/download-60.cgi 下载 Tomcat 6,并将其解压到任意文件夹 $tomcatHome(例如,在 opt/shib-tomcat 文件夹中)。

    需要注意的是,目前当 SP 和 IdP 通过 SOAP 直接通信时,不能使用 Tomcat 7.*。尽管在本文章的示例中,我们将使用直接浏览器重定向来执行这些通信,但我们仍然建议使用 Tomcat 6 版本。

  2. $shDistr/endorsed 文件夹复制到 $tomcatHome 文件夹。
  3. 修改 $tomcatHome/bin/setenv.sh 文件以指定最小 JVM 内存参数。
    JAVA_OPTS="$JAVA_OPTS -Xmx512m -XX:MaxPermSize=128m"
  4. 下载库:(https://build.shibboleth.net/nexus/content/repositories/releases/edu/internet2/middleware/security/tomcat6/tomcat6-dta-ssl/1.0.0/tomcat6-dta-ssl-1.0.0.jar) 到 $tomcatHome/lib 文件夹,用于 SP 和 IdP 之间通信的 SOAP 协议支持。
  5. 现在让我们打开 $tomcatHome/conf/server.xml 并设置通过 HTTPS 访问 Tomcat。为此,我们定义以下 Connector 元素:
    <Connector port="8443"
           protocol="org.apache.coyote.http11.Http11Protocol"
           SSLImplementation="edu.internet2.middleware.security.tomcat6.DelegateToApplicationJSSEmplementation"
           scheme="https"
           SSLEnabled="true"
           clientAuth="want"
               keystoreFile="$shHome/credentials/idp.jks"
               keystorePass="$shPassword" />

    别忘了将变量 $shHome $shPassword 替换为其真实值。

  6. 现在是将 Shibboleth IdP 应用程序部署到 Tomcat。为此,我们创建一个文件 $tomcatHome/conf/Catalina/localhost/idp.xml,内容如下:
    <Context docBase="$shHome/war/idp.war"
    privileged="true"
    antiResourceLocking="false"
    antiJARLocking="false"
    unpackWAR="false"
    swallowOutput="true" />

    别忘了将变量 $shHome 替换为正确的值。

  7. 编译以下类并将其打包到一个 jar 文件 tomcat-valve.jar
      public class RemoteUserValve extends ValveBase{
        public RemoteUserValve() {
            }
    
            @Override
            public void invoke(final Request request, final Response response)
             throws IOException, ServletException {
               final String username = "idpuser";
            final String credentials = "idppass";
            final List<String> roles = new ArrayList<String>();
            final Principal principal = new GenericPrincipal(null, username, credentials,
                                                                  roles);
    
            request.setUserPrincipal(principal);
            getNext().invoke(request, response);
          }
        }

    将 jar 文件放入 ${tomcatHome}/lib 文件夹。在 server.xml 文件中添加以下代码行:

    <Valve ?lassName=”com.eastbanctech.java.web.RemoteUserValve” />

    <Host name=«localhost» appBase=«webapps» ...> 元素内部。

    此 Valve 仅用于确保 Tomcat 为每个请求设置 REMOTE_USER 变量,从而模拟 IdP 的身份验证过程。

5. 实现 SAML 2.0 协议的 SP Filter

现在,我们将实现一个 SAML 2.0 服务提供者 Servlet Filter,它负责以下任务:

  1. Filter 允许不需身份验证的公共资源请求通过。
  2. Filter 缓存已验证用户的身份信息,以减少对 Shibboleth IdP 的请求数量。
  3. Filter 创建 SAML 2.0 身份验证请求消息 (Authn) 并通过浏览器重定向将其传递给 Shibboleth IdP。
  4. Filter 处理来自 Shibboleth IdP 的响应消息,如果用户身份验证成功,则显示最初请求的资源。
  5. 当用户从 Java Web 应用程序注销时,Filter 会删除本地会话。

同时,Shibboleth IdP 会话保持活动状态。

从技术角度来看,Filter 将是标准接口 javax.filter.Filter 的实现。Filter 的范围将在特定的 Web 应用程序上设置。

现在 Filter 的功能已经明确,让我们开始实现吧。

  1. 创建一个 Maven 项目结构。

    可以使用 Maven archetypes 插件来完成此操作。

    mvn archetype:generate -DgroupId=ru.eastbanctech.java.web 
    -DartifactId=saml-sp-filter -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false

    您可以根据自己的喜好指定 `groupId` 和 `artefactId` 参数。

    我们在 Intellij Idea 中的项目结构将如下所示:

  2. 主 pom.xml:
    <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>
        <groupId>ru.eastbanctech.web</groupId>
        <artifactId>saml-sp-filter</artifactId>
        <name>${project.artifactId}</name>
        <version>1.0-SNAPSHOT</version>
        <packaging>jar</packaging>
        <properties>
            <!-- General settings -->
            <jdk.version>1.6</jdk.version>
            <encoding>UTF-8</encoding>
            <project.build.sourceEncoding>${encoding}</project.build.sourceEncoding>
            <project.reporting.outputEncoding>${encoding}</project.reporting.outputEncoding>
        </properties>
        <build>
          <pluginManagement>
            <plugins>
              <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.5.1</version>
                <configuration>
                  <encoding>${encoding}</encoding>
                  <source>${jdk.version}</source>
                  <target>${jdk.version}</target>
                </configuration>
              </plugin>
            </plugins>
          </pluginManagement>
        </build>
        <dependency>
          <groupId>org.opensaml</groupId>
          <artifactId>opensaml</artifactId>
          <version>2.5.1-1</version>
        </dependency>
    
        <dependency>
          <groupId>javax.servlet</groupId>
          <artifactId>servlet-api</artifactId>
          <version>2.5</version>
          <scope>provided</scope>
        </dependency>
            <dependency>
              <groupId>org.slf4j</groupId>
              <artifactId>log4j-over-slf4j</artifactId>
              <version>1.7.1</version>
            </dependency>
    
            <dependency>
              <groupId>org.slf4j</groupId>
              <artifactId>slf4j-api</artifactId>
              <version>1.7.1</version>
            </dependency>
        </dependencies>
    </project>
  3. SAMLSPFilter 是我们 Filter 的核心。
    public class SAMLSPFilter implements Filter {
       public static final String SAML_AUTHN_RESPONSE_PARAMETER_NAME = "SAMLResponse";
       private static Logger log = LoggerFactory.getLogger(SAMLSPFilter.class);
       private FilterConfig filterConfig;
       private SAMLResponseVerifier checkSAMLResponse;
       private SAMLRequestSender samlRequestSender;
    
       @Override
       public void init(javax.servlet.FilterConfig config) throws ServletException {
         OpenSamlBootstrap.init();
         filterConfig = new FilterConfig(config);
         checkSAMLResponse = new SAMLResponseVerifier();
         samlRequestSender = new SAMLRequestSender();
       }
    
       @Override
       public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
                              FilterChain chain) throws IOException, ServletException {
         HttpServletRequest request = (HttpServletRequest) servletRequest;
         HttpServletResponse response = (HttpServletResponse) servletResponse;
       /*
         Step 1: Ignore requests that are not addressed to the filter
         Step 2: If the answer comes from Shibboleth idP, we handle it
         Step 3: If you receive a request for logout, delete the local session
         Step 4: If the user has already been authenticated, then we can grant access to a resource
         Step 5: Create a SAML запрос на аутентификацию and send the user to Shibboleth idP
      */ 
    }
    }

    FilterConfig 指定了基本的 Filter 参数(排除的资源、IdP 名称、IdP 元数据路径、SP 名称等)。这些变量的值在 Java Web 应用程序的配置文件 web.xml 中指定。

    对象 checkSAMLResponse samlRequestSender 用于检查 SAML 2.0 消息的有效性并发送身份验证请求。我们稍后会回来讨论它们。

    public class FilterConfig {
      /**
      * The parameters below should be defined in web.xml file of Java Web Application
      */
      public static final String EXCLUDED_URL_PATTERN_PARAMETER = "excludedUrlPattern";
      public static final String SP_ACS_URL_PARAMETER           = "acsUrl";
      public static final String SP_ID_PARAMETER                = "spProviderId";
      public static final String SP_LOGOUT_URL_PARAMETER        = "logoutUrl";
      public static final String IDP_SSO_URL_PARAMETER          = "idProviderSSOUrl";
    
      private String excludedUrlPattern;
      private String acsUrl;
      private String spProviderId;
      private String logoutUrl;
      private String idpSSOUrl;
    
      public FilterConfig(javax.servlet.FilterConfig config) {
        excludedUrlPattern = config.getInitParameter(EXCLUDED_URL_PATTERN_PARAMETER);
        acsUrl = config.getInitParameter(SP_ACS_URL_PARAMETER);
        spProviderId = config.getInitParameter(SP_ID_PARAMETER);
        idpSSOUrl = config.getInitParameter(IDP_SSO_URL_PARAMETER);
        logoutUrl = config.getInitParameter(SP_LOGOUT_URL_PARAMETER);
      }
      // getters and should be defined below
    }
    OpenSamlBootstrap class initializes libraries to work with SAML 2.0 messages:
    public class OpenSamlBootstrap extends DefaultBootstrap {
      private static Logger log = LoggerFactory.getLogger(OpenSamlBootstrap.class);
      private static boolean initialized;
      private static String[] xmlToolingConfigs = {
        "/default-config.xml",
        "/encryption-validation-config.xml",
        "/saml2-assertion-config.xml",
        "/saml2-assertion-delegation-restriction-config.xml",
        "/saml2-core-validation-config.xml",
        "/saml2-metadata-config.xml",
        "/saml2-metadata-idp-discovery-config.xml",
        "/saml2-metadata-query-config.xml",
        "/saml2-metadata-validation-config.xml",
        "/saml2-protocol-config.xml",
        "/saml2-protocol-thirdparty-config.xml",
        "/schema-config.xml",
        "/signature-config.xml",
        "/signature-validation-config.xml"
      };
    
      public static synchronized void init() {
        if (!initialized) {
          try {
            initializeXMLTooling(xmlToolingConfigs);
          } catch (ConfigurationException e) {
            log.error("Unable to initialize opensaml DefaultBootstrap", e);
          }
          initializeGlobalSecurityConfiguration();
          initialized = true;
        }
      }
    }

    xmlToolingConfigs 数组中指定的 XML 文件包含关于如何处理 SAML 2.0 消息元素的操作说明。XML 文件本身位于 opensaml-*.jar 库中。

步骤 1:忽略未被 Filter 处理的请求。

excludedUrlPattern 参数是一个正则表达式。如果请求的资源 URL 与 excludedUrlPattern 匹配,Filter 将忽略该请求。

if (!isFilteredRequest(request)) {
  log.debug("According to {} configuration parameter request is ignored + {}",
      new Object[]{FilterConfig.EXCLUDED_URL_PATTERN, request.getRequestURI()});
  chain.doFilter(servletRequest, servletResponse);
  return;
}

// We add method to the filter class that will check if the request needs to be handled
private boolean isFilteredRequest(HttpServletRequest request) {
  return !(filterConfig.getExcludedUrlPattern() != null &&
            getCorrectURL(request).matches(filterConfig.getExcludedUrlPattern()));
   }
   // Also add the auxiliary method for receiving the correct URL
private String getCorrectURL(HttpServletRequest request) {
  String contextPath = request.getContextPath();
  String requestUri = request.getRequestURI();
  int contextBeg = requestUri.indexOf(contextPath);
  int contextEnd = contextBeg + contextPath.length();
  String slash = "/";
  String url = (contextBeg < 0 || contextEnd == (requestUri.length() - 1))
                    ? requestUri : requestUri.substring(contextEnd);
  if (!url.startsWith(slash)) {
      url = slash + url;
  }
  return url;
}

步骤 2:如果收到来自 Shibboleth IdP 的响应,我们进行处理。

我们在 HTTP 请求中查找“SAMLResponse”。如果找到,则表示我们已收到 Shibboleth IdP 对身份验证请求的响应。然后我们开始处理 SAML 2.0 消息。

log.debug("Attempt to secure resource  is intercepted : {}", 
((HttpServletRequest) servletRequest).getRequestURL().toString());
/*
  Check if response message is received from identity provider;
  In case of successful response system redirects user to relayState (initial) request
*/
String responseMessage = servletRequest.getParameter(SAML_AUTHN_RESPONSE_PARAMETER_NAME);
if (responseMessage != null) {
  log.debug("Response from Identity Provider is received");
  try {
    log.debug("Decoding of SAML message");
    SAMLMessageContext samlMessageContext =
                    SAMLUtils.decodeSamlMessage((HttpServletRequest) servletRequest,
                                                (HttpServletResponse) servletResponse);
    log.debug("SAML message has been decoded successfully");
    samlMessageContext.setLocalEntityId(filterConfig.getSpProviderId());
    String relayState = getInitialRequestedResource(samlMessageContext);
    checkSAMLResponse.verify(samlMessageContext);
    log.debug("Starting and store SAML session..");
    SAMLSessionManager.getInstance().createSAMLSession(request.getSession(),
                                                                   samlMessageContext);
    log.debug("User has been successfully authenticated in idP. Redirect to initial
                                                   requested resource {}", relayState);
    response.sendRedirect(relayState);
    return;
  } catch (Exception e) {
       throw new ServletException(e);
  }
} 

我们需要使用 SAMLUtils.decodeSamlMessage(..) 方法解码 SAML 消息,并验证包含的 SAML 断言——checkSAMLResponse.verify(..)。当一切验证完毕后,我们创建一个本地 SAML 会话 SAMLSessionManager.getInstance().createSAMLSession(..),并将用户重定向到最初请求的资源 response.sendRedirect (..)

SAMLUtils 类中,我们包含用于 SAML 2.0 消息处理的有用辅助方法。其中之一是 decodeSamlMessage ,它解码通过 HTTPS 接收的 SAML 2.0 消息。

public class SAMLUtils {
  public static SAMLMessageContext decodeSamlMessage(HttpServletRequest request,                  
  HttpServletResponse response) throws Exception {

    SAMLMessageContext<SAMLObject, SAMLObject, NameID> samlMessageContext =
                 new BasicSAMLMessageContext<SAMLObject, SAMLObject, NameID>();

    HttpServletRequestAdapter httpServletRequestAdapter =
                                          new HttpServletRequestAdapter(request);
    samlMessageContext.setInboundMessageTransport(httpServletRequestAdapter);
    samlMessageContext.setInboundSAMLProtocol(SAMLConstants.SAML20P_NS);
    HttpServletResponseAdapter httpServletResponseAdapter =
                     new HttpServletResponseAdapter(response, request.isSecure());
    samlMessageContext.setOutboundMessageTransport(httpServletResponseAdapter);
    samlMessageContext.setPeerEntityRole(IDPSSODescriptor.DEFAULT_ELEMENT_NAME);

    SecurityPolicyResolver securityPolicyResolver =
                                    getSecurityPolicyResolver(request.isSecure());

    samlMessageContext.setSecurityPolicyResolver(securityPolicyResolver);
    HTTPPostDecoder samlMessageDecoder = new HTTPPostDecoder();
    samlMessageDecoder.decode(samlMessageContext);
    return samlMessageContext;
  }

  private static SecurityPolicyResolver getSecurityPolicyResolver(boolean isSecured) {
    SecurityPolicy securityPolicy = new BasicSecurityPolicy();
    HTTPRule httpRule = new HTTPRule(null, null, isSecured);
    MandatoryIssuerRule mandatoryIssuerRule = new MandatoryIssuerRule();
    List<SecurityPolicyRule> securityPolicyRules = securityPolicy.getPolicyRules();
    securityPolicyRules.add(httpRule);
    securityPolicyRules.add(mandatoryIssuerRule);
    return new StaticSecurityPolicyResolver(securityPolicy);
  }
}

让我们在此类中添加一个将 SAML 对象转换为 String 的附加方法。这对于记录 SAML 消息很有用。

public static String SAMLObjectToString(XMLObject samlObject) {
  try {
    Marshaller marshaller =
        org.opensaml.Configuration.getMarshallerFactory().getMarshaller(samlObject);
    org.w3c.dom.Element authDOM = marshaller.marshall(samlObject);
    StringWriter rspWrt = new StringWriter();
    XMLHelper.writeNode(authDOM, rspWrt);
    return rspWrt.toString();
  } catch (Exception e) {
    e.printStackTrace();
  }
  return null;
}

创建一个类 SAMLResponseVerifier,它将包含从 Shibboleth IdP 接收的 SAML 2.0 消息的消息验证功能。在主方法 verify (..) 中,我们实现以下验证:

  • IdP SAML 2.0 消息是对我们 Filter 发送的某个 SAML 2.0 请求的响应。
  • 消息包含 Shibboleth IdP 对用户的正面身份验证。
  • SAML 2.0 响应中的主要断言已满足(消息未过期,消息是为我们的 SP 准备的,等等)。
public class SAMLResponseVerifier {
  private static Logger log = LoggerFactory.getLogger(SAMLResponseVerifier.class);
  private SAMLRequestStore samlRequestStore = SAMLRequestStore.getInstance();

  public void verify(SAMLMessageContext<Response, SAMLObject, NameID> samlMessageContext)
    throws SAMLException {
    Response samlResponse = samlMessageContext.getInboundSAMLMessage();
    log.debug("SAML Response message : {}", SAMLUtils.SAMLObjectToString(samlResponse));
    verifyInResponseTo(samlResponse);
    Status status = samlResponse.getStatus();
    StatusCode statusCode = status.getStatusCode();
    String statusCodeURI = statusCode.getValue();
    if (!statusCodeURI.equals(StatusCode.SUCCESS_URI)) {
      log.warn("Incorrect SAML message code : {} ",
                   statusCode.getStatusCode().getValue());
      throw new SAMLException("Incorrect SAML message code : " + statusCode.getValue());
    }
    if (samlResponse.getAssertions().size() == 0) {
      log.error("Response does not contain any acceptable assertions");
      throw new SAMLException("Response does not contain any acceptable assertions");
    }

    Assertion assertion = samlResponse.getAssertions().get(0);
    NameID nameId = assertion.getSubject().getNameID();
    if (nameId == null) {
      log.error("Name ID not present in subject");
      throw new SAMLException("Name ID not present in subject");
    }
    log.debug("SAML authenticated user " + nameId.getValue());
    verifyConditions(assertion.getConditions(), samlMessageContext);
}

private void verifyInResponseTo(Response samlResponse) {
  String key = samlResponse.getInResponseTo();
  if (!samlRequestStore.exists(key)) { {
    log.error("Response does not match an authentication request");
    throw new RuntimeException("Response does not match an authentication request");
  }
  samlRequestStore.removeRequest(samlResponse.getInResponseTo());
}

private void verifyConditions(Conditions conditions, SAMLMessageContext samlMessageContext) throws SAMLException{
    verifyExpirationConditions(conditions);
    verifyAudienceRestrictions(conditions.getAudienceRestrictions(), samlMessageContext);
}
private void verifyExpirationConditions(Conditions conditions) throws SAMLException {
  log.debug("Verifying conditions");
  DateTime currentTime = new DateTime(DateTimeZone.UTC);
  log.debug("Current time in UTC : " + currentTime);
  DateTime notBefore = conditions.getNotBefore();
  log.debug("Not before condition : " + notBefore);
  if ((notBefore != null) && currentTime.isBefore(notBefore))
    throw new SAMLException("Assertion is not conformed with notBefore condition");

  DateTime notOnOrAfter = conditions.getNotOnOrAfter();
  log.debug("Not on or after condition : " + notOnOrAfter);
  if ((notOnOrAfter != null) && currentTime.isAfter(notOnOrAfter))
    throw new SAMLException("Assertion is not conformed with notOnOrAfter condition");
}

 private void verifyAudienceRestrictions(
 List<AudienceRestriction> audienceRestrictions,
            SAMLMessageContext<?, ?, ?> samlMessageContext)
            throws SAMLException{
// TODO: Audience restrictions should be defined below
}
}

verifyInResponseTo 方法验证 SAML 2.0 响应是否 preceded by our filter's request。实现时,使用了 SAMLRequestStore 类的对象,该对象存储发送到 Shibboleth IdP 的 SAML 2.0 请求。

final public class SAMLRequestStore {
  private Set<String> samlRequestStorage = new HashSet<String>();
  private IdentifierGenerator identifierGenerator = new RandomIdentifierGenerator();
  private static SAMLRequestStore instance = new SAMLRequestStore();

  private SAMLRequestStore() {
  }

  public static SAMLRequestStore getInstance() {
    return instance;
  }

  public synchronized void storeRequest(String key) {
    if (samlRequestStorage.contains(key))
     throw new RuntimeException("SAML request storage has already contains key " + key);

    samlRequestStorage.add(key);
  }
  public synchronized String storeRequest(){
     String key = null;
     while (true) {
       key = identifierGenerator.generateIdentifier(20);
       if (!samlRequestStorage.contains(key)){
         storeRequest(key);
         break;
       }
     }
    return key;
  }
  public synchronized boolean exists(String key) {
    return samlRequestStorage.contains(key);
  }

  public synchronized void removeRequest(String key) {
    samlRequestStorage.remove(key);
  }
}

为了创建本地会话,我们将使用我们自己的类 SAMLSessionManager。其目的是使用 SAMLContext 在 Servlet 会话上下文中创建/销毁由 SAMLSessionInfo 类对象表示的本地会话。

public class SAMLSessionInfo {
  private String nameId;
  private Map<String, String> attributes;
  private Date validTo;
  public SAMLSessionInfo(String nameId, Map<String, String> attributes, Date validTo) {
    this.nameId = nameId;
    this.attributes = attributes;
    this.validTo = validTo;
  }
   // getters should be defined below
}

SAMLSessionManager 类使用 SAMLContext 在 Servlet 会话上下文中创建和销毁本地 SAML 会话。

public class SAMLSessionManager {
  public static String SAML_SESSION_INFO = "SAML_SESSION_INFO";
  private static SAMLSessionManager instance = new SAMLSessionManager();

  private SAMLSessionManager() {
  }

  public static SAMLSessionManager getInstance() {
    return instance;
  }

  public void createSAMLSession(HttpSession session, SAMLMessageContext<Response,
         SAMLObject, NameID> samlMessageContext) {
    List<Assertion> assertions =
                       samlMessageContext.getInboundSAMLMessage().getAssertions();
    NameID nameId = (assertions.size() != 0 && assertions.get(0).getSubject() != null) ?
                     assertions.get(0).getSubject().getNameID() : null;
    String nameValue = nameId == null ? null : nameId.getValue();
    SAMLSessionInfo samlSessionInfo = new SAMLSessionInfo(nameValue,
                                      getAttributesMap(getSAMLAttributes(assertions)),
                                      getSAMLSessionValidTo(assertions));
    session.setAttribute(SAML_SESSION_INFO, samlSessionInfo);
  }

  public boolean isSAMLSessionValid(HttpSession session) {
        SAMLSessionInfo samlSessionInfo = (SAMLSessionInfo)
                              session.getAttribute(SAML_SESSION_INFO);
        if (samlSessionInfo == null)
            return false;
        return samlSessionInfo.getValidTo() == null || new
                 Date().before(samlSessionInfo.getValidTo());
  }

  public void destroySAMLSession(HttpSession session) {
    session.removeAttribute(SAML_SESSION_INFO);
  }

  public List<Attribute> getSAMLAttributes(List<Assertion> assertions) {
    List<Attribute> attributes = new ArrayList<Attribute>();
    if (assertions != null) {
      for (Assertion assertion : assertions) {
        for (AttributeStatement attributeStatement :
                                 assertion.getAttributeStatements()) {
          for (Attribute attribute : attributeStatement.getAttributes()) {
             attributes.add(attribute);
          }
        }
     }
   }
   return attributes;
 }

 public Date getSAMLSessionValidTo(List<Assertion> assertions) {
   org.joda.time.DateTime sessionNotOnOrAfter = null;
   if (assertions != null) {
     for (Assertion assertion : assertions) {
       for (AuthnStatement statement : assertion.getAuthnStatements()) {
         sessionNotOnOrAfter = statement.getSessionNotOnOrAfter();
       }
     }
   }

   return sessionNotOnOrAfter != null ?
           sessionNotOnOrAfter.toCalendar(Locale.getDefault()).getTime() : null;
 }
 public Map<String, String> getAttributesMap(List<Attribute> attributes) {
   Map<String, String> result = new HashMap<String, String>();
   for (Attribute attribute : attributes) {
     result.put(attribute.getName(), attribute.getDOM().getTextContent());
   }
   return result;
 }
}

步骤 3:如果收到注销请求,则删除本地会话。

if (getCorrectURL(request).equals(filterConfig.getLogoutUrl())) {
  log.debug("Logout action: destroying SAML session.");
  SAMLSessionManager.getInstance().destroySAMLSession(request.getSession());
  chain.doFilter(request, response);
  return;
}

注意:会话在 Shibboleth IdP 上保持活动状态,并在提示身份验证时,Shibboleth IdP 会简单地将我们重定向到一个活动会话。全局注销的实现需要额外的配置,而 Shibboleth IdP 在 2.4.0 版本之前不支持这些配置。您可以在 此处了解更多信息。

步骤 4:如果用户已通过身份验证,则我们可以授予对资源的访问权限。

如果用户在我们的 Filter 中具有活动的 SAML 会话,我们将向用户提供该资源。

if (SAMLSessionManager.getInstance().isSAMLSessionValid(request.getSession())) {
     log.debug("SAML session exists and valid: grant access to secure resource");
     chain.doFilter(request, response);
     return;
}

步骤 5:创建 SAML 身份验证请求并将用户发送到 Shibboleth IdP。

log.debug("Sending authentication request to idP");
try {
  samlRequestSender .sendSAMLAuthRequest(request, response,
    filterConfig.getSpProviderId(), filterConfig.getAcsUrl(),
    filterConfig.getIdpSSOUrl());
} catch (Exception e) {
   throw new ServletException(e);
 }

SAMLRequestSender 类创建、编码(可能加密和/或签名)并作为 SAML 2.0 消息发送请求。

public class SAMLRequestSender {
  private static Logger log = LoggerFactory.getLogger(SAMLRequestSender.class);
  private SAMLAuthnRequestBuilder samlAuthnRequestBuilder =
                                                   new SAMLAuthnRequestBuilder();
  private MessageEncoder messageEncoder = new MessageEncoder();

  public void sendSAMLAuthRequest(HttpServletRequest request, HttpServletResponse
     servletResponse, String spId, String acsUrl, String idpSSOUrl) throws Exception {
    String redirectURL;
    String idpUrl = idpSSOUrl;
    AuthnRequest authnRequest = samlAuthnRequestBuilder.buildRequest(spId, acsUrl,
                                   idpUrl);
    // store SAML 2.0 authentication request
    String key = SAMLRequestStore.getInstance().storeRequest();
    authnRequest.setID(key);
    log.debug("SAML Authentication message : {} ",
                            SAMLUtils.SAMLObjectToString(authnRequest));
    redirectURL = messageEncoder.encode(authnRequest, idpUrl, request.getRequestURI());

    HttpServletResponseAdapter responseAdapter =
                    new HttpServletResponseAdapter(servletResponse, request.isSecure());
    HTTPTransportUtils.addNoCacheHeaders(responseAdapter);
    HTTPTransportUtils.setUTF8Encoding(responseAdapter);
    responseAdapter.sendRedirect(redirectURL);
  }

  private static class SAMLAuthnRequestBuilder {

    public AuthnRequest buildRequest(String spProviderId, String acsUrl, String idpUrl){
      /* Building Issuer object */
      IssuerBuilder issuerBuilder = new IssuerBuilder();
      Issuer issuer =
                     issuerBuilder.buildObject("urn:oasis:names:tc:SAML:2.0:assertion",
                                                "Issuer", "saml2p");
      issuer.setValue(spProviderId);

      /* Creation of AuthRequestObject */
      DateTime issueInstant = new DateTime();
      AuthnRequestBuilder authRequestBuilder = new AuthnRequestBuilder();

      AuthnRequest authRequest =
                    authRequestBuilder.buildObject(SAMLConstants.SAML20P_NS,
                            "AuthnRequest", "saml2p");
      authRequest.setForceAuthn(false);
      authRequest.setIssueInstant(issueInstant);
      authRequest.setProtocolBinding(SAMLConstants.SAML2_POST_BINDING_URI);
      authRequest.setAssertionConsumerServiceURL(acsUrl);
      authRequest.setIssuer(issuer);
      authRequest.setNameIDPolicy(nameIdPolicy);
      authRequest.setVersion(SAMLVersion.VERSION_20);
      authRequest.setDestination(idpUrl);

      return authRequest;
    }
  }

  private static class MessageEncoder extends HTTPRedirectDeflateEncoder {
    public String encode(SAMLObject message, String endpointURL, String relayState)
                                         throws MessageEncodingException {
      String encodedMessage = deflateAndBase64Encode(message);
      return buildRedirectURL(endpointURL, relayState, encodedMessage);
    }
    public String buildRedirectURL(String endpointURL, String relayState, String message)
                                                        throws MessageEncodingException {
      URLBuilder urlBuilder = new URLBuilder(endpointURL);
      List<Pair<String, String>> queryParams = urlBuilder.getQueryParams();
      queryParams.clear();
      queryParams.add(new Pair<String, String>("SAMLRequest", message));
      if (checkRelayState(relayState)) {
         queryParams.add(new Pair<String, String>("RelayState", relayState));
      }
      return urlBuilder.buildURL();
    }
  }

}

包含用户身份验证指令的 SAML 2.0 消息使用 buildRequest 方法构建,实际上是一个 XML 对象。

<saml2p:AuthnRequest xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol"
     AssertionConsumerServiceURL="https://sp.local.ru:8443/sso/acs"
     Destination="https://idp.local.ru:8443/idp/profile/SAML2/Redirect/SSO"
     ForceAuthn="false"
     ID="_0ddb303f9500839762eabd30e7b1e3c28b596c69"
     IssueInstant="2013-09-12T09:46:41.882Z"
     ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Version="2.0">
    <saml2p:Issuer
       xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:assertion">sp.local.ru</saml2p:Issuer>
</saml2p:AuthnRequest>

AssertionConsumerServiceURL 参数指定 Shibboleth IdP 返回响应的 URL,而 parametrProtocolBinding 参数指示如何将响应返回给我们 Filter(在本例中为 HTTP POST)。

ID 参数指定消息标识符。我们在发送消息时保存它 `String key = SAMLRequestStore.getInstance().storeRequest();` 并在分析消息时使用 SAMLResponseVerifier 类的 verifyInResponseTo 方法进行验证。

saml2p:Issuer 元素指定我们 SP 的名称/实体 ID。使用 saml2p:Issuer 的值,Shibboleth IdP 确定身份验证请求是从哪个 SP 发送的,以及它应该如何处理(通过元数据 SP)。

我们将从 IdP 接收 SAML 2.0 消息的 XML 格式作为响应。

<saml2p:Response xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol"
     Destination="https://sp.local.ru:8443/sso/acs"
     ID="_9c5e6028df334510cce22409ddbca6ac"
     InResponseTo="_0ddb303f9500839762eabd30e7b1e3c28b596c69"
     IssueInstant="2013-09-12T10:13:35.177Z" Version="2.0">
   <saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion"
             Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">
                     https://idp.local.ru/idp/shibboleth
   </saml2:Issuer>
<saml2p:Status>
    <saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</saml2p:Status>
<saml2:Assertion xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion"
    ID="_0a299e86f4b17b5e047735121a880ccb" IssueInstant="2013-09-12T10:13:35.177Z"
    version="2.0">
    <saml2:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">
          https://idp.local.ru/idp/shibboleth
    </saml2:Issuer>
    <saml2:Subject>
      <saml2:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
        NameQualifier="https://idp.local.ru/idp/shibboleth">
        _f1de09ee54294d4b5ddeb3aa5e6d2aab
      </saml2:NameID>
      <saml2:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
        <saml2:SubjectConfirmationData Address="127.0.0.1"
          InResponseTo="_0ddb303f9500839762eabd30e7b1e3c28b596c69"
          NotOnOrAfter="2013-09-12T10:18:35.177Z"
          Recipient="https://sp.local.ru:8443/sso/acs"/>
      </saml2:SubjectConfirmation>
    </saml2:Subject>
    <saml2:Conditions
         NotBefore="2013-09-12T10:13:35.177Z"
         NotOnOrAfter="2013-09-12T10:18:35.177Z">
        <saml2:AudienceRestriction>
            <saml2:Audience>sp.local.ru</saml2:Audience>
        </saml2:AudienceRestriction>
    </saml2:Conditions>
    <saml2:AuthnStatement AuthnInstant="2013-09-12T10:13:35.137Z"
              SessionIndex="_91826738984ca8bef18a8450135b1821">
        <saml2:SubjectLocality Address="127.0.0.1"/>
        <saml2:AuthnContext>
          <saml2:AuthnContextClassRef>
            urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
          </saml2:AuthnContextClassRef>
        </saml2:AuthnContext>
    </saml2:AuthnStatement>
 <saml2:AttributeStatement>
        <saml2:Attribute Name="userLogin" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
            <saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">idpuser</saml2:AttributeValue>
        </saml2:Attribute>
    </saml2:AttributeStatement>
</saml2:Assertion>
</saml2p:Response>

消息将在 SAMLResponseVerifier.verify(..) 方法中处理。

就这样——我们的 Filter 已经实现了!

我们的项目结构如下所示:

我们将 Filter 构建为本地存储库中的 jar 库。

为此,我们在 c.pom.xml 所在的目录中运行命令:`mvn clean install`。

6. 创建支持 SSO 的 Java Web 应用程序

创建 Java Web 应用程序

为了说明这一点,我们将创建一个简单的 Java Web 应用程序,其中包含 private public 资源。访问 private 资源需要通过 Shibboleth IdP Web 应用程序进行用户身份验证。其中一个 private 资源将是一个显示系统当前用户信息页面的页面。

我们的应用程序项目结构如下所示:

pom.xml

<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/maven-v4_0_0.xsd">

<modelVersion>4.0.0</modelVersion>
<groupId> ru.eastbanctech.web</groupId>
<artifactId>SimpleSSOApplication</artifactId>
<packaging>war</packaging>
<version>1.0-SNAPSHOT</version>
<name>SimpleSSOApplication</name>
<url>http://maven.apache.org</url>

<!-- Determine the value for our application -->
<properties>
  <sp.id>sp.local.ru</sp.id>
  <acs.url>https://sp.local.ru:8443/sso/acs</acs.url>
  <idp.sso.url>https://idp.local.ru:8443/idp/profile/SAML2/Redirect/SSO</idp.sso.url>
  <logout.url>/logout</logout.url>
</properties>

<dependencies>
  <dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>servlet-api</artifactId>
    <version>2.5</version>
   </dependency>
   <dependency>
     <groupId> ru.eastbanctech.web</groupId>
     <artifactId>saml-sp-filter</artifactId>
     <version>1.0-SNAPSHOT</version>
   </dependency>
   <dependency>
     <groupId>org.slf4j</groupId>
     <artifactId>slf4j-api</artifactId>
     <version>1.7.1</version>
   </dependency>
   <dependency>
     <groupId>org.slf4j</groupId>
     <artifactId>slf4j-log4j12</artifactId>
     <version>1.7.1</version>
   </dependency>
 </dependencies>

 <build>
   <finalName>sso</finalName>
   <plugins>
     <plugin>
       <artifactId>maven-war-plugin</artifactId>
       <configuration>
         <webResources>
           <resource>
             <filtering>true</filtering>
             <directory>src/main/webapp/WEB-INF</directory>
             <targetPath>WEB-INF</targetPath>
             <includes>
               <include>**/*.xml</include>
             </includes>
           </resource>
         </webResources>
       </configuration>
     </plugin>
   </plugins>
 </build>
 </project>

现在,需要注意会话属性,其中设置了我们 Filter 的基本参数。

 <sp.id>sp.local.ru</sp.id> - the name/entityId of SAML 2.0 filter SP
 <acs.url>https://sp.local.ru:8443/sso/acs</acs.url> - URL of the filter to process SAML 2.0 messages from Shibboleth IdP
 <idp.sso.url>https://idp.local.ru:8443/idp/profile/SAML2/Redirect/SSO</idp.sso.url> - URL for our filter to send Shibboleth IdP messages
 <logout.url>/logout</logout.url> - logout URL

web.xml

web.xml 文件中,我们定义了 Filter 的参数及其范围。我们通过 excludedUrlPattern 参数排除格式为“*.jpg”的资源。

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
  <display-name>Simple SSO Java Web Application</display-name>7.
  <filter>
    <filter-name>SSOFilter</filter-name>
    <filter-class> ru.eastbanctech.java.web.filter.saml.SAMLSPFilter</filter-class>
    <init-param>
      <param-name>excludedUrlPattern</param-name>
      <param-value>.*\.jpg</param-value>
   </init-param>
   <init-param>
     <param-name>idProviderSSOUrl</param-name>
     <param-value> ${idp.sso.url}</param-value>
   </init-param>
   <init-param>
     <param-name>spProviderId</param-name>
     <param-value>${sp.id}</param-value>
   </init-param>
   <init-param>
     <param-name>acsUrl</param-name>
     <param-value>${acs.url}</param-value>
   </init-param>
   <init-param>
     <param-name>logoutUrl</param-name>
     <param-value>${logout.url}</param-value>
   </init-param>
 </filter>
 <filter-mapping>
   <filter-name>SSOFilter</filter-name>
   <url-pattern>/pages/private/*</url-pattern>
 </filter-mapping>

 <filter-mapping>
   <filter-name>SSOFilter</filter-name>
   <url-pattern>${logout.url}</url-pattern>
 </filter-mapping>

 <filter-mapping>
   <filter-name>SSOFilter</filter-name>
   <url-pattern>/acs</url-pattern>
 </filter-mapping>
</web-app>

private/page.jsp

该页面仅显示用户 ID 和已验证用户的属性。

<%@ page import=" ru.eastbanctech.java.web.filter.saml.store.SAMLSessionManager" %>
<%@ page import=" ru.eastbanctech.java.web.filter.saml.store.SAMLSessionInfo" %>
<%@ page import="java.util.Map" %>
<html>
<body>
<h2>Private Resource</h2>
<%
  SAMLSessionInfo info =
       (SAMLSessionInfo)request.getSession().getAttribute(SAMLSessionManager.SAML_SESSION_INFO);
  out.println("User id = " + info.getNameId() + "<BR/>");
  out.println("<TABLE> <TR> <TH> Attribute name </TH> <TH> Attribulte value </TH></TR>");

  for (Map.Entry entry : info.getAttributes().entrySet()) {
    out.println("<TR><TD>" + entry.getKey() + "</TD><TD>" + entry.getValue() + "</TD></TR>");
  }
  out.println("</TABLE>");
%>

<a href="<%=request.getContextPath()%>/logout">Logout</a>
</body>
</html>

让我们使用命令:`mvn clean package` 来构建应用程序。

测试 Java Web 应用程序性能

让我们将应用程序部署到 Tomcat AS 并测试 SSO 性能。

  1. 我们在 ${tomcatHome}/conf/Catalina/localhost/sso.xml 文件中描述了应用程序上下文。
    <Context docBase="$pathToWebApp" privileged="true" antiResourceLocking="false"
            antiJARLocking="false"    unpackWAR="false" swallowOutput="true" />

    或者,只需将我们的 sso.war 应用程序复制到 ${tomcatHome}/webapps

  2. 为了让 Tomcat 应用程序通过 HTTPS 协议连接到 Shibboleth IdP,有必要将 Shibboleth IdP 证书添加到 Java 受信任的密钥库中。

    我们使用 Java keytool 工具来实现这一点。

    keytool -alias idp.local.ru -importcert -file ${shHome}/idp.crt -keystore ${keystorePath}
  3. 启动 Tomcat AS。
  4. 打开浏览器窗口并访问受保护的应用程序资源 https://sp.local.ru:8443/sso/pages/private/page.jsp
  5. 确保页面已打开,并且我们可以看到用户 ID 和用户名。

  6. 作为练习,请确保 Filter 允许访问 /pages/private 文件夹中格式为 .jpg 的图片请求。
  7. 与 Google Apps 集成

现在是时候确认我们的 SSO 确实在工作了。为此,让我们使用 Google Apps 作为另一个服务提供者(http://www.google.com/enterprise/apps/business/)。

  1. 注册您的域名和超级管理员,使用免费试用版本。完成后,使用这些凭据(完全限定域名)登录 http://admin.google.com/
  2. 在管理面板中使用“创建用户”并为 idpuser 分配超级管理员权限。
  3. 在屏幕底部,从下拉菜单中选择“添加控件”,然后选择“安全”。

  4. 然后选择“高级设置”->“设置 SSO”。
  5. 勾选“启用 SSO”并设置以下参数:
    Entry Page URL * = https://idp.local.ru:8443/idp/profile/SAML2/Redirect/SSO
    Exit Page URL * =  gmail.com
    Change Password URL * =  gmail.com

    点击“保存更改”按钮。

  6. 下载证书以通过 HTTPS 与 Shibboleth IdP 配合使用。证书位于 $shHome/credentials/idp.crt

    点击“保存更改”按钮。

  7. 使用 此处的说明,配置 Shibboleth IdP 以与 Google Apps 配合使用。

    注意:为添加的元素指定命名空间,否则在 Shibboleth IdP 启动时会出错。例如,而不是 RelyingParty,您需要写 rp: RelyingParty

  8. 对于名为 edu.internet2.middleware.shibboleth 的记录器,设置 DEBUG 级别。
        <!-- Logs IdP, but not OpenSAML, messages -->
        <logger name="edu.internet2.middleware.shibboleth" level="DEBUG"/>
  9. 重启 Shibboleth IdP 并在新的浏览器会话中访问页面 https://admin.google.com(您可能需要删除 cookies 或在 Google Chrome 中使用隐身模式)。
  10. 输入 `idpuser@domain_name`,其中 domain_name 是您的注册域名,然后输入密码。按“Enter”。
  11. 接受未签名的证书,并确保您已作为 idpuser 登录 Google Apps。

    在 Shibboleth 日志 ${shHome}/logs/idp-process.log 中,您应该可以看到 Shibboleth IdP 如何处理您的请求。在那里,您将看到身份验证过程是通过 RemoteUserLoginHandler 进行的。

    22:19:49.172 - DEBUG [edu.internet2.middleware.shibboleth.idp.authn.provider.RemoteUserLoginHandler:66] - Redirecting to https://idp.local.ru:8443/idp/Authn/RemoteUser

    总的来说,Shibboleth IdP 中的所有日志都非常简单且信息丰富。我们建议花一些时间来理解它们。

  12. 然后,我们打开我们的应用程序 URL https://sp.local.ru:8443/sso/pages/private/page.jsp,并在日志中查看 Shibboleth IdP 正在为用户 idpuser 查找可用会话。

好了,这就是全部。我们简单的 SSO 系统正在运行。希望您发现本文有用。

有用链接

  1. https://developers.google.com/google-apps/sso/saml_reference_implementation – Google Apps 的 SSO 服务。它解释了如何使用 SAML 将 SSO 与 Google Docs 集成。
  2. https://shibboleth.usc.edu/docs/google-apps/ – 关于 Shibboleth 和 Google Docs 集成的说明。
  3. http://stackoverflow.com/questions/7553967/getting-a-value-from-httpservletrequest-getremoteuser-in-tomcat-without-modify - 如何实现您的 Tomcat Valve。
  4. https://wiki.shibboleth.net/confluence/display/SHIB2/Home - Shibboleth 文档。

¬或者,您通常可以使用 IdP 提供的通用服务提供者集成模块。在 Shibboleth 的情况下,这意味着需要在应用程序服务器前面安装一个附加的 Apache 服务器,并配置 mod_shib 模块。

  • 在撰写本文时,Shibboleth IdP 2.4.0 是最新版本。
  • 我们在环境中使用了 Java 7。
  • 我们使用 CentOS 6.3 作为我们的操作系统。它也在 Ubuntu 12.04 上进行了测试。编译需要 servlet-api 2.5 和 ${tomcatHome}/lib/catalina.jar
  • 我们建议您作为练习自己实现它。
  • 根据您的环境更改红色设置。
© . All rights reserved.