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






4.62/5 (5投票s)
通过 SAML 2.0 资源共享 SSO 集成经验
1. 背景
尽管单点登录 (SSO) 已经存在、被讨论和使用很长时间,但实践表明它并非总是易于实现。本文的目的是展示如何为 SAML 2.0 身份提供者 (IdP) 实现自定义的服务提供者 (SP),并利用它将 SAML SSO 集成到您的 Java Web 应用程序中。
在我们最近的一个项目中,我们必须为一所主要大学部署一个集群门户解决方案。其中一项任务是为以下系统实现 SSO:
- Liferay 门户
- 简单的自定义 Java Web 应用程序
- Google Apps
包括附加要求和注意事项
- Shibboleth 已被选定并用作 IdP。Shibboleth 是一个开源系统,完全实现了 SAML 1.0 和 SAML 2.0 协议(http://shibboleth.net/about/index.html)。
- SAML 2.0 被选为 SSO 集成的主要协议。
- Jasig CAS 已配置为 Shibboleth 身份验证提供者。
除了更灵活的身份验证用户体验和流程配置外,这种设置还使 CAS 兼容的系统能够参与 SSO,而无需进行额外实现。
LDAP 已配置为 CAS 的身份验证提供者,并为 Shibboleth 配置为属性提供者。
我们面临的挑战包括
- 制造商提供的 Shibboleth 文档并不总是最新和完整的。
- 很少有关于 Java Web 应用程序的 SAML SP 实现的全面示例。
- 使用与 CAS 集成的 Shibboleth 来启用 SAML 和 CAS SSO 会使 SSO 会话管理复杂化,并需要特别注意。
在解决了这些困难之后,我们忍不住与读者分享我们的经验,以便让其他开发人员更容易理解和使用 SAML 2.0。
2. 目标受众
- 使用 SAML 2.0 将 SSO 功能集成到其项目中的开发人员。
- 正在为他们的应用程序寻找使用 SAML 2.0 集成 SSO 功能的实际示例的 Java 开发人员。
- 希望尝试 Shibboleth 作为 SAML 身份提供者 (IdP) 的 Java 开发人员。
为了更好地理解本文,建议具备 SAML 2.0 规范的基本知识。
3. SSO 主要组件
下图展示了系统中 SSO 的通用操作。

图表显示
- 2 个参与 SSO 的应用程序
- Java Web App – 部署到 Apache Tomcat 的基于 Java Servlet 的 Web 应用程序
- Google Apps – Google 云服务
- SP Filter – 服务提供者实现。它通过 SAML 2.0 协议与 SAML Shibboleth IdP 交互
- Shibboleth IdP 是用于通过 SAML 1.0 和 SAML 2.0 进行身份验证和授权的应用程序。
- Tomcat AS 是 Java 应用程序服务器。
- SP Filter 和 Shibboleth IdP 之间的交互通过安全的 HTTPS 协议进行。
请注意,图表显示 Shibboleth IdP 和 Java Web 应用程序部署在不同的 Tomcat 服务器上。但是,您可以使用单个 Tomcat 实例在一个节点上部署环境。
4. 设置 Shibboleth IdP 环境
安装和配置 Shibboleth IdP
- 从 http://shibboleth.net/downloads/identity-provider/latest/ 下载最新 IdP 版本并解压到任意目录;我们将使用
$shDistr
来指代该位置。 - 检查
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) 的密码。
- 现在,作为 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。属性提供者的配置在此 处有更详细的描述。
- 我们还需要修改 $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。
- 我们的 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 服务,并且它不会对消息和响应进行签名或加密。 - 在此步骤中,我们将指定 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 发送。
- 现在剩下的是选择 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
- 首先,从 https://tomcat.net.cn/download-60.cgi 下载 Tomcat 6,并将其解压到任意文件夹 $tomcatHome(例如,在 opt/shib-tomcat 文件夹中)。
需要注意的是,目前当 SP 和 IdP 通过 SOAP 直接通信时,不能使用 Tomcat 7.*。尽管在本文章的示例中,我们将使用直接浏览器重定向来执行这些通信,但我们仍然建议使用 Tomcat 6 版本。
- 将 $shDistr/endorsed 文件夹复制到 $tomcatHome 文件夹。
- 修改 $tomcatHome/bin/setenv.sh 文件以指定最小 JVM 内存参数。
JAVA_OPTS="$JAVA_OPTS -Xmx512m -XX:MaxPermSize=128m"
- 下载库:(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 协议支持。
- 现在让我们打开 $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
替换为其真实值。 - 现在是将 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
替换为正确的值。 - 编译以下类并将其打包到一个 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,它负责以下任务:
- Filter 允许不需身份验证的公共资源请求通过。
- Filter 缓存已验证用户的身份信息,以减少对 Shibboleth IdP 的请求数量。
- Filter 创建 SAML 2.0 身份验证请求消息 (
Authn
) 并通过浏览器重定向将其传递给 Shibboleth IdP。 - Filter 处理来自 Shibboleth IdP 的响应消息,如果用户身份验证成功,则显示最初请求的资源。
- 当用户从 Java Web 应用程序注销时,Filter 会删除本地会话。
同时,Shibboleth IdP 会话保持活动状态。
从技术角度来看,Filter 将是标准接口 javax.filter.Filter
的实现。Filter 的范围将在特定的 Web 应用程序上设置。
现在 Filter 的功能已经明确,让我们开始实现吧。
- 创建一个 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 中的项目结构将如下所示:
- 主 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>
- 类
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 性能。
- 我们在 ${tomcatHome}/conf/Catalina/localhost/sso.xml 文件中描述了应用程序上下文。
<Context docBase="$pathToWebApp" privileged="true" antiResourceLocking="false" antiJARLocking="false" unpackWAR="false" swallowOutput="true" />
或者,只需将我们的
sso.war
应用程序复制到 ${tomcatHome}/webapps。 - 为了让 Tomcat 应用程序通过 HTTPS 协议连接到 Shibboleth IdP,有必要将 Shibboleth IdP 证书添加到 Java 受信任的密钥库中。
我们使用 Java keytool 工具来实现这一点。
keytool -alias idp.local.ru -importcert -file ${shHome}/idp.crt -keystore ${keystorePath}
- 启动 Tomcat AS。
- 打开浏览器窗口并访问受保护的应用程序资源 https://sp.local.ru:8443/sso/pages/private/page.jsp。
- 确保页面已打开,并且我们可以看到用户 ID 和用户名。
- 作为练习,请确保 Filter 允许访问 /pages/private 文件夹中格式为 .jpg 的图片请求。
- 与 Google Apps 集成
现在是时候确认我们的 SSO 确实在工作了。为此,让我们使用 Google Apps 作为另一个服务提供者(http://www.google.com/enterprise/apps/business/)。
- 注册您的域名和超级管理员,使用免费试用版本。完成后,使用这些凭据(完全限定域名)登录 http://admin.google.com/。
- 在管理面板中使用“创建用户”并为 idpuser 分配超级管理员权限。
- 在屏幕底部,从下拉菜单中选择“添加控件”,然后选择“安全”。
- 然后选择“高级设置”->“设置 SSO”。
- 勾选“启用 SSO”并设置以下参数:
Entry Page URL * = https://idp.local.ru:8443/idp/profile/SAML2/Redirect/SSO Exit Page URL * = gmail.com Change Password URL * = gmail.com
点击“保存更改”按钮。
- 下载证书以通过 HTTPS 与 Shibboleth IdP 配合使用。证书位于 $shHome/credentials/idp.crt。
点击“保存更改”按钮。
- 使用 此处的说明,配置 Shibboleth IdP 以与 Google Apps 配合使用。
注意:为添加的元素指定命名空间,否则在 Shibboleth IdP 启动时会出错。例如,而不是
RelyingParty
,您需要写rp: RelyingParty
。 - 对于名为
edu.internet2.middleware.shibboleth
的记录器,设置 DEBUG 级别。<!-- Logs IdP, but not OpenSAML, messages --> <logger name="edu.internet2.middleware.shibboleth" level="DEBUG"/>
- 重启 Shibboleth IdP 并在新的浏览器会话中访问页面 https://admin.google.com(您可能需要删除 cookies 或在 Google Chrome 中使用隐身模式)。
- 输入 `idpuser@domain_name`,其中
domain_name
是您的注册域名,然后输入密码。按“Enter”。 - 接受未签名的证书,并确保您已作为 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 中的所有日志都非常简单且信息丰富。我们建议花一些时间来理解它们。
- 然后,我们打开我们的应用程序 URL https://sp.local.ru:8443/sso/pages/private/page.jsp,并在日志中查看 Shibboleth IdP 正在为用户 idpuser 查找可用会话。
好了,这就是全部。我们简单的 SSO 系统正在运行。希望您发现本文有用。
有用链接
- https://developers.google.com/google-apps/sso/saml_reference_implementation – Google Apps 的 SSO 服务。它解释了如何使用 SAML 将 SSO 与 Google Docs 集成。
- https://shibboleth.usc.edu/docs/google-apps/ – 关于 Shibboleth 和 Google Docs 集成的说明。
- http://stackoverflow.com/questions/7553967/getting-a-value-from-httpservletrequest-getremoteuser-in-tomcat-without-modify - 如何实现您的 Tomcat Valve。
- 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。
- 我们建议您作为练习自己实现它。
- 根据您的环境更改红色设置。