本页将大致介绍安全断言标记语言 (SAML) 2.0 Building Block 以及 SAML 身份验证提供程序的常见单点登录 (SSO) 问题和故障排除技术。

如果出于任何原因,将更新/全新的 IdP 元数据 XML 文件上传到 Blackboard Learn GUI 的“SAML 验证设置”页面(位于 SAML 验证提供程序的“身份验证提供程序设置”部分),则还应将 SAML B2 和该 SAML 验证提供程序切换为“非活动/可用”,并且时 SAML 身份验证提供程序处于“活动”状态,以确保任何缓存的 IdP 元数据得到清除及更新的 IdP 元数据得以充分利用。


关键术语

本指南中将会使用以下术语和缩写:

  • SAML:安全断言标记语言
  • IdP:身份验证提供程序
  • SP:服务提供程序
  • ADFS:Active Directory 联合身份验证服务
  • GUI:图形用户界面。在 Blackboard Learn 上下文中,这表示在软件内运作。

编辑 SAML 配置设置

为了帮助排除 SAML 验证问题,我们在 3200.2.0 版本中更新了 SAML Building Block,以纳入以下配置设置和选项:

  • 定义 SAML 会话时间限制
  • 选择签名算法类型
  • 重新生成证书
  • 更改 ResponseSkew 值

详细了解如何在 SAML Building Block 中配置设置


错误和异常情况

与 SAML 相关的错误/异常情况会捕获在以下日志中:

  • /usr/local/blackboard/logs/bb-services-log.txt
  • /usr/local/blackboard/logs/tomcat/stdout-stderr-<date>.log
  • /usr/local/blackboard/logs/tomcat/catalina-log.txt

当调查报告的 SAML 验证问题时,这些日志应始终可供搜索。


SAML 跟踪器

对于 SAML 2.0 验证故障排除迭代,在某种程度上,可能需要确认/查看实际上来自 IdP 并在验证过程中发送给 Learn 的属性。如果来自 IdP 的属性在 SAML 响应中未经过加密,则 Firefox 浏览器 SAML 跟踪器附加组件Chrome SAML Message Decoder 可用于查看此类属性。


未正确映射的属性

如果包含 userName 的属性未正确映射为远程用户 ID 字段(位于 Blackboard Learn GUI 上 SAML 验证设置页面的映射 SAML 属性部分)中指定的内容,则以下事件会在尝试通过 SAML 验证登录到 Blackboard Learn 时记录在 bb-services 日志中:

2016-06-28 12:48:12 -0400 - userName is null or empty

浏览器会显示以下类似登录错误!消息:Blackboard Learn 目前无法使用单点登录来登录到您的帐户。请与您的管理员联系寻求帮助。

浏览器中显示的登录错误信息的图片,该信息称 Blackboard Learn 目前无法使用单点登录来登录您的帐户。请联系您的管理员寻求帮助。

在 bb-services 日志中会显示 Authentication Failure 条目:

2016-06-28 12:48:12 -0400 - BbSAMLExceptionHandleFilter - javax.servlet.ServletException: Authentication Failure
    at blackboard.auth.provider.saml.customization.handler.BbAuthenticationSuccessHandler.checkAuthenticationResult(BbAuthenticationSuccessHandler.java:81)
    at blackboard.auth.provider.saml.customization.handler.BbAuthenticationSuccessHandler.onAuthenticationSuccess(BbAuthenticationSuccessHandler.java:57)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.successfulAuthentication(AbstractAuthenticationProcessingFilter.java:331)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:245)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:184)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:64) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:53) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:91)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:176)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
[SNIP]

解决方法

解决此类问题的方法有两种。第一种,在 Blackboard Learn GUI 的 SAML 验证设置页面上选择如果系统中没有帐户,则创建帐户选项。第二种,如果属性未经过加密,则您可以尝试通过 SAML 跟踪器或调试日志记录查看来自 IdP 的属性值:

<saml2:Attribute Name="urn:oid:0.9.2342.19200300.100.1.3">
    <saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
                          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                          xsi:type="xs:anyType"
                          >bbuser_saml2@bbchjones.net</saml2:AttributeValue>
</saml2:Attribute>

并将 Attribute Name(含有所需的 AttributeValue)映射到远程用户 ID(位于 Blackboard Learn GUI 的 SAML 验证设置页面)。


未选择的兼容数据源

如果在 Blackboard Learn GUI 上 SAML 验证设置页面的服务提供程序设置 > 兼容数据源部分未选择用户的数据源,则用户将无法通过 SAML 验证登录到 Blackboard Learn。以下事件将在尝试通过 SAML 验证登录到 Blackboard Learn 时记录到 bb-services 日志中:

2016-09-23 12:33:13 -0500 - userName is null or empty

浏览器中会显示登录错误!消息,bb-services 日志中会记录验证失败条目:

2016-09-23 12:33:13 -0500 - BbSAMLExceptionHandleFilter - javax.servlet.ServletException: Authentication Failure
    at blackboard.auth.provider.saml.customization.handler.BbAuthenticationSuccessHandler.checkAuthenticationResult(BbAuthenticationSuccessHandler.java:82)
    at blackboard.auth.provider.saml.customization.handler.BbAuthenticationSuccessHandler.onAuthenticationSuccess(BbAuthenticationSuccessHandler.java:58)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.successfulAuthentication(AbstractAuthenticationProcessingFilter.java:331)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:245)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:184)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:64)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:53)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter (SecurityContextPersistenceFilter.java:91)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:176)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262)
    at sun.reflect.GeneratedMethodAccessor3399.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
        [SNIP]

解决方法

  1. 获取无法登录的用户的用户名。
  2. 在 Blackboard Learn GUI 中,导航到系统管理员 > 用户并搜索用户。
  3. 复制用户的数据源密钥
  4. 导航到系统管理员 > 验证 >“提供程序名称”> SAML 设置 > 兼容数据源
  5. 选中名称列中该数据源旁边的复选标记,然后选择提交

“指定的 URL 不完整”错误消息

如果在 Blackboard Learn 中将 OneLogin 配置为 SAML 验证提供程序的 IdP,则在尝试登录 Blackboard Learn 时输入 OneLogin 凭证后,指定的 URL 不完整错误可能会显示在页面上。

bb-services-log 中会显示以下内容:

2016-09-16 09:43:40 -0400 - Given URL is not well formed<P><span class="captionText">For reference, the Error ID is 17500f44-7809-4b9f-a272-3bed1d1af131.</span> - java.lang.IllegalArgumentException: Given URL is not well formed
    at org.opensaml.util.URLBuilder.<init>(URLBuilder.java:120)
    at org.opensaml.util.SimpleURLCanonicalizer.canonicalize(SimpleURLCanonicalizer.java:87)
    at org.opensaml.common.binding.decoding.BasicURLComparator.compare(BasicURLComparator.java:57)
    at org.opensaml.common.binding.decoding.BaseSAMLMessageDecoder.compareEndpointURIs(BaseSAMLMessageDecoder.java:173)
    at org.opensaml.common.binding.decoding.BaseSAMLMessageDecoder.checkEndpointURI(BaseSAMLMessageDecoder.java:213)
    at org.opensaml.saml2.binding.decoding.BaseSAML2MessageDecoder.decode(BaseSAML2MessageDecoder.java:72)
    at org.springframework.security.saml.processor.SAMLProcessorImpl.retrieveMessage(SAMLProcessorImpl.java:105)
    at org.springframework.security.saml.processor.SAMLProcessorImpl.retrieveMessage(SAMLProcessorImpl.java:172)
    at org.springframework.security.saml.SAMLProcessingFilter.attemptAuthentication(SAMLProcessingFilter.java:80)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:217)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:184)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:64)
    [SNIP]
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:745)
Caused by: java.net.MalformedURLException: no protocol: {recipient}
    at java.net.URL.<init>(URL.java:593)
    at java.net.URL.<init>(URL.java:490)
    at java.net.URL.<init>(URL.java:439)
    at org.opensaml.util.URLBuilder.<init>(URLBuilder.java:77)
        ... 203 more

解决方法

  1. 启用 Firefox 浏览器 SAML 跟踪器并复制登录问题。
  2. 检查 SAML POST 事件的开头:

    <samlp:Response Destination="{recipient}"
            ID="R8afbfbfee7292613f98ad4ec4115de7c6b385be6"
            InResponseTo="a3g2424154bb0gjh3737ii66dadbff4"
            IssueInstant="2016-09-16T18:49:09Z"
            Version="2.0"
            xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
            xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
            >
        <saml:Issuer>https://app.onelogin.com/saml/metadata/123456</saml:Issuer>
        [SNIP]

  3. 对于包含“Response”的第 1 行,注意观察 Destination= 仅被设置为收件人。
  4. 让客户端访问其 OneLogin IdP 的配置部分。
  5. 确认收件人字段为空。
  6. 复制 ACS(使用者)URL 的值,将其粘贴到收件人字段,然后选择保存

IdP/SP 问题场景

  1. 如果错误发生在系统将您重定向到 IdP 登录页面之前,则 IdP 的元数据可能无效。
  2. 如果错误发生在您登录到 IdP 页面之后,则原因可能是:
    1. 在 SP 与 IdP 之间映射的属性不正确,或者 IdP 未返回有效的“远程用户 ID”。
    2. 来自 IdP 的 SAML 响应未经过 SP 的验证。这可能是由以下情况导致的:
      • IdP 使用未由有效证书颁发机构签发的证书对 SAML 响应签名,而且 SP 的密钥库不包含这个证书。
      • SP 的系统时钟不正确。

Active Directory 联合身份验证服务 (ADFS)

映射 SAML 属性部分(位于 Blackboard Learn GUI 的 SAML 验证设置页面)中的属性名称区分大小写。因此,如果远程用户 ID 在设置页面上的属性名称值为 sAMAccountName,且来自 IdP 的实际 SAML POST 在 AttributeStatement 中的属性名称值也是该值:

<AttributeStatement>
    <Attribute Name="SamAccountName>
        <AttributeValue>Test-User</AttributeValue>
    </Attribute>
</AttributeStatement>

用户将无法登录。SAML 验证设置页面上的远程用户 ID 属性名称值需要从 sAMAccountName 更改为 SamAccountName

“找不到资源”或“登录错误!”警告

本部分的一些常见问题可能会在 Blackboard Learn GUI 显示找不到指定的资源,或者您无权访问该资源登录错误!消息时,阻止用户通过 SAML 验证与 ADFS 登录 Learn。

第 1 个问题

在 ADFS 登录页面输入登录凭证后,系统可能会在重定向到 Blackboard Learn GUI 后显示错误:找不到指定的资源,或者您无权访问该资源。

找不到指定的资源错误消息

stdout-stderr 日志中会记录相应消息:

INFO | jvm 1 | 2016/06/22 06:08:33 | - No mapping found for HTTP request with URI [/auth-saml/saml/SSO] in DispatcherServlet with name 'saml'

之所以发生此问题,是因为在 DispatcherServlet.java 代码中使用了 noHandlerFound() 方法,且该方法无法找到/映射 HTTP SSO 请求。

/**
 * No handler found -> set appropriate HTTP response status.
 * @param request current HTTP request
 * @param response current HTTP response
 * @throws Exception if preparing the response failed
 */
protected void noHandlerFound(HttpServletRequest request, HttpServletResponse response) throws Exception {
 if (pageNotFoundLogger.isWarnEnabled()) {
  pageNotFoundLogger.warn("No mapping found for HTTP request with URI [" + getRequestUri(request) +
    "] in DispatcherServlet with name '" + getServletName() + "'");
 }
 if (this.throwExceptionIfNoHandlerFound) {
  throw new NoHandlerFoundException(request.getMethod(), getRequestUri(request),
    new ServletServerHttpRequest(request).getHeaders());
 }
 else {
  response.sendError(HttpServletResponse.SC_NOT_FOUND);
 }
}

解决方法

这种问题经常发生的原因是,在 Blackboard Learn GUI 中配置的 SP 的实体 ID 不正确。要解决此问题,只需导航到系统管理员 > 验证 > SAML 验证设置 > 服务提供程序设置并更新实体 ID 即可。对于 ADFS, 实体 ID的默认配置是 https://[Learn Server Hostname]/auth-saml/saml/SSO

如果学校将自己的 URL 从默认 https://school.blackboard.com 更改为 https://their.school.edu,则 Blackboard Learn GUI 上“SAML 验证设置”页面的实体 ID 应更新为 https://their.school.edu/auth-saml/saml/SSO。

第 2 个问题

在 ADFS 登录页面输入登录凭证后,系统可能会在重定向到 Blackboard Learn GUI 后显示错误:找不到指定的资源,或者您无权访问该资源。

stdout-stderr 日志中会记录这条相应消息:

INFO  | jvm 1  | 2016/06/22 06:08:33 | - No mapping found for HTTP request with URI [/auth-saml/saml/SSO] in DispatcherServlet with name 'saml'

catalina 日志中会显示如下消息:

ERROR 2016-06-27 10:47:03,664 connector-6: userId=_2_1, sessionId=62536416FB80462298C92064A7022E50 org.opensaml.xml.encryption.Decrypter - Error decrypting the encrypted data element
org.apache.xml.security.encryption.XMLEncryptionException: Illegal key size
Original Exception was java.security.InvalidKeyException: Illegal key size
    at org.apache.xml.security.encryption.XMLCipher.decryptToByteArray(XMLCipher.java:1822)
    at org.opensaml.xml.encryption.Decrypter.decryptDataToDOM(Decrypter.java:596)
    at org.opensaml.xml.encryption.Decrypter.decryptUsingResolvedEncryptedKey(Decrypter.java:795)
    at org.opensaml.xml.encryption.Decrypter.decryptDataToDOM(Decrypter.java:535)
    at org.opensaml.xml.encryption.Decrypter.decryptDataToList(Decrypter.java:453)
    at org.opensaml.xml.encryption.Decrypter.decryptData(Decrypter.java:414)
    at org.opensaml.saml2.encryption.Decrypter.decryptData(Decrypter.java:141)
    at org.opensaml.saml2.encryption.Decrypter.decrypt(Decrypter.java:69)
    at org.springframework.security.saml.websso.WebSSOProfileConsumerImpl.processAuthenticationResponse(WebSSOProfileConsumerImpl.java:199)
    at org.springframework.security.saml.SAMLAuthenticationProvider.authenticate(SAMLAuthenticationProvider.java:82)
    at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:167)
        [SNIP]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:745)
Caused by: java.security.InvalidKeyException: Illegal key size
    at javax.crypto.Cipher.checkCryptoPerm(Cipher.java:1039)
    at javax.crypto.Cipher.init(Cipher.java:1393)
    at javax.crypto.Cipher.init(Cipher.java:1327)
    at org.apache.xml.security.encryption.XMLCipher.decryptToByteArray(XMLCipher.java:1820)
        ... 205 more

bb-services 日志中会显示如下消息:

2016-06-27 10:47:03 -0400 - unsuccessfulAuthentication - org.springframework.security.authentication.AuthenticationServiceException: Error validating SAML message
    at org.springframework.security.saml.SAMLAuthenticationProvider.authenticate(SAMLAuthenticationProvider.java:100)
    at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:167)
    at org.springframework.security.saml.SAMLProcessingFilter.attemptAuthentication(SAMLProcessingFilter.java:87)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:217)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:184)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:64)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:53)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:91)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:176)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262)
    at sun.reflect.GeneratedMethodAccessor3422.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.apache.catalina.security.SecurityUtil$1.run(SecurityUtil.java:277)
    at org.apache.catalina.security.SecurityUtil$1.run(SecurityUtil.java:274)
    at java.security.AccessController.doPrivileged(Native Method)
    at javax.security.auth.Subject.doAsPrivileged(Subject.java:549)
    at org.apache.catalina.security.SecurityUtil.execute(SecurityUtil.java:309)
    at org.apache.catalina.security.SecurityUtil.doAsPrivilege(SecurityUtil.java:249)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:237)
    at org.apache.catalina.core.ApplicationFilterChain.access$000(ApplicationFilterChain.java:55)
    at org.apache.catalina.core.ApplicationFilterChain$1.run(ApplicationFilterChain.java:191)
    at org.apache.catalina.core.ApplicationFilterChain$1.run(ApplicationFilterChain.java:187)
    at java.security.AccessController.doPrivileged(Native Method)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:186)
    at blackboard.auth.provider.saml.customization.filter.BbSAMLExceptionHandleFilter.doFilterInternal(BbSAMLExceptionHandleFilter.java:30)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at sun.reflect.GeneratedMethodAccessor3421.invoke(Unknown Source)
        [SNIP]
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:745)
Caused by: org.opensaml.common.SAMLException: Response doesn't have any valid assertion which would pass subject validation
    at org.springframework.security.saml.websso.WebSSOProfileConsumerImpl.processAuthenticationResponse(WebSSOProfileConsumerImpl.java:229)
    at org.springframework.security.saml.SAMLAuthenticationProvider.authenticate(SAMLAuthenticationProvider.java:87)
        ... 229 more

之所以发生这个问题是因为,默认情况下,ADFS 使用 AES-256 对所发送的属性加密,而 Blackboard Learn 使用的 Java 运行时又不支持立即可用的 AES-256。

解决方法

通用解决方法就是,在 ADFS 服务器上打开 PowerShell,并设置为 Blackboard Learn 创建的信赖方,以未加密的形式发送属性。由于整个通信都是通过 SSL 进行的,因此这不会降低验证的安全性。它还可以使任何问题的调试变得更加轻松,因为属性可以通过调试工具(如 Firefox 浏览器 SAML 跟踪器附加组件)进行查看,而且无需重新启动 Blackboard Learn 系统。要设置为 Blackboard Learn 创建的信赖方来以未加密形式发送属性,请打开 PowerShell 并执行以下命令,以将 TargetName 替换为信赖方信任的名称(位于“ADFS 管理控制台”的信任关系 > 信赖方信任下方)。

set-ADFSRelyingPartyTrust –TargetName "yourlearnserver.blackboard.com" –EncryptClaims $False

做出上述更改后,需要通过以下命令重新启动 ADFS 服务: Restart-Service ADFSSRV

第 3 个问题

在 ADFS 登录页面输入登录凭证后,系统可能会在重定向到 Blackboard Learn GUI 后显示错误:找不到指定的资源,或者您无权访问该资源登录错误!

对于上述任一种情况,在 stdout-stderr 日志中会显示以下与 SAML 相关的类似相应事件:

INFO   | jvm 1    | 2016/09/06 20:33:04 | - /saml/login?apId=_107_1&redirectUrl=https%3A%2F%2Fbb.fraser.misd.net%2Fwebapps%2Fportal%2Fexecute%2FdefaultTab at position 1 of 10 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter'
INFO   | jvm 1    | 2016/09/06 20:33:04 | - No HttpSession currently exists
INFO   | jvm 1    | 2016/09/06 20:33:04 | - No SecurityContext was available from the HttpSession: null. 将创建一个新项。
INFO   | jvm 1    | 2016/09/06 20:33:04 | - /saml/login?apId=_107_1&redirectUrl=https%3A%2F%2Fbb.fraser.misd.net%2Fwebapps%2Fportal%2Fexecute%2FdefaultTab at position 2 of 10 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter'
INFO   | jvm 1    | 2016/09/06 20:33:04 | - /saml/login?apId=_107_1&redirectUrl=https%3A%2F%2Fbb.fraser.misd.net%2Fwebapps%2Fportal%2Fexecute%2FdefaultTab at position 3 of 10 in additional filter chain; firing Filter: 'HeaderWriterFilter'
INFO   | jvm 1    | 2016/09/06 20:33:04 | - /saml/login?apId=_107_1&redirectUrl=https%3A%2F%2Fbb.fraser.misd.net%2Fwebapps%2Fportal%2Fexecute%2FdefaultTab at position 4 of 10 in additional filter chain; firing Filter: 'FilterChainProxy'
INFO   | jvm 1    | 2016/09/06 20:33:04 | - Checking match of request : '/saml/login'; against '/saml/login/**'
INFO   | jvm 1    | 2016/09/06 20:33:04 | - /saml/login?apId=_107_1&redirectUrl=https%3A%2F%2Fbb.fraser.misd.net%2Fwebapps%2Fportal%2Fexecute%2FdefaultTab at position 1 of 1 in additional filter chain; firing Filter: 'SAMLEntryPoint'
INFO   | jvm 1    | 2016/09/06 20:33:04 | - Request for URI http://www.w3.org/2000/09/xmldsig#rsa-sha1
INFO   | jvm 1    | 2016/09/06 20:33:04 | - Request for URI http://www.w3.org/2000/09/xmldsig#rsa-sha1
INFO   | jvm 1    | 2016/09/06 20:33:04 | - SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.
INFO   | jvm 1    | 2016/09/06 20:33:04 | - SecurityContextHolder now cleared, as request processing completed
INFO   | jvm 1    | 2016/09/06 20:33:07 | - /saml/SSO at position 1 of 10 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter'
INFO   | jvm 1    | 2016/09/06 20:33:07 | - HttpSession returned null object for SPRING_SECURITY_CONTEXT
INFO   | jvm 1    | 2016/09/06 20:33:07 | - No SecurityContext was available from the HttpSession: org.apache.catalina.session.StandardSessionFacade@6708a718. 将创建一个新项。
INFO   | jvm 1    | 2016/09/06 20:33:07 | - /saml/SSO at position 2 of 10 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter'
INFO   | jvm 1    | 2016/09/06 20:33:07 | - /saml/SSO at position 3 of 10 in additional filter chain; firing Filter: 'HeaderWriterFilter'
INFO   | jvm 1    | 2016/09/06 20:33:07 | - /saml/SSO at position 4 of 10 in additional filter chain; firing Filter: 'FilterChainProxy'
INFO   | jvm 1    | 2016/09/06 20:33:07 | - Checking match of request : '/saml/sso'; against '/saml/login/**'
INFO   | jvm 1    | 2016/09/06 20:33:07 | - Checking match of request : '/saml/sso'; against '/saml/logout/**'
INFO   | jvm 1    | 2016/09/06 20:33:07 | - Checking match of request : '/saml/sso'; against '/saml/bbsamllogout/**'
INFO   | jvm 1    | 2016/09/06 20:33:07 | - Checking match of request : '/saml/sso'; against '/saml/sso/**'
INFO   | jvm 1    | 2016/09/06 20:33:07 | - /saml/SSO at position 1 of 1 in additional filter chain; firing Filter: 'SAMLProcessingFilter'
INFO   | jvm 1    | 2016/09/06 20:33:07 | - Authentication attempt using org.springframework.security.saml.SAMLAuthenticationProvider
INFO   | jvm 1    | 2016/09/06 20:33:07 | - Forwarding to /
INFO   | jvm 1    | 2016/09/06 20:33:07 | - DispatcherServlet with name 'saml' processing POST request for [/auth-saml/saml/SSO]
INFO   | jvm 1    | 2016/09/06 20:33:07 | - No mapping found for HTTP request with URI [/auth-saml/saml/SSO] in DispatcherServlet with name 'saml'
INFO   | jvm 1    | 2016/09/06 20:33:07 | - SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.
INFO   | jvm 1    | 2016/09/06 20:33:07 | - Successfully completed request
INFO   | jvm 1    | 2016/09/06 20:33:07 | - Skip invoking on
INFO   | jvm 1    | 2016/09/06 20:33:07 | - SecurityContextHolder now cleared, as request processing completed

或者,bb-services 日志中会显示如下类似 SAML 异常:

2016-11-29 09:04:24 -0500 - unsuccessfulAuthentication - org.springframework.security.authentication.AuthenticationServiceException: Error validating SAML message
    at org.springframework.security.saml.SAMLAuthenticationProvider.authenticate(SAMLAuthenticationProvider.java:100)
    at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:167)
    at org.springframework.security.saml.SAMLProcessingFilter.attemptAuthentication(SAMLProcessingFilter.java:87)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:217)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:184)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:64)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:53)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:91)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:176)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262)
    at sun.reflect.GeneratedMethodAccessor853.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.apache.catalina.security.SecurityUtil$1.run(SecurityUtil.java:282)
    at org.apache.catalina.security.SecurityUtil$1.run(SecurityUtil.java:279)
    at java.security.AccessController.doPrivileged(Native Method)
    at javax.security.auth.Subject.doAsPrivileged(Subject.java:549)
    at org.apache.catalina.security.SecurityUtil.execute(SecurityUtil.java:314)
    at org.apache.catalina.security.SecurityUtil.doAsPrivilege(SecurityUtil.java:253)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:190)
    at org.apache.catalina.core.ApplicationFilterChain.access$000(ApplicationFilterChain.java:46)
    at org.apache.catalina.core.ApplicationFilterChain$1.run(ApplicationFilterChain.java:148)
    at org.apache.catalina.core.ApplicationFilterChain$1.run(ApplicationFilterChain.java:144)
    at java.security.AccessController.doPrivileged(Native Method)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:143)
    at blackboard.auth.provider.saml.customization.filter.BbSAMLExceptionHandleFilter.doFilterInternal(BbSAMLExceptionHandleFilter.java:30)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at sun.reflect.GeneratedMethodAccessor853.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        [SNIP]
    at org.apache.catalina.valves.RemoteIpValve.invoke(RemoteIpValve.java:677)
    at blackboard.tomcat.valves.LoggingRemoteIpValve.invoke(LoggingRemoteIpValve.java:44)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:349)
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:1110)
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:785)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1425)
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:745)
Caused by: org.opensaml.common.SAMLException: Response issue time is either too old or with date in the future, skew 60, time 2016-11-29T14:03:16.634Z
    at org.springframework.security.saml.websso.WebSSOProfileConsumerImpl.processAuthenticationResponse(WebSSOProfileConsumerImpl.java:126)
    at blackboard.auth.provider.saml.customization.consumer.BbSAMLWebSSOProfileConsumerImpl.processAuthenticationResponse(BbSAMLWebSSOProfileConsumerImpl.java:40)
    at org.springframework.security.saml.SAMLAuthenticationProvider.authenticate(SAMLAuthenticationProvider.java:87)
        ... 230 more

如果 ADFS 服务器和 Blackboard Learn 应用程序服务器的时间偏移接近或超过 60 秒默认值,则会发生此问题。

解决方法

解决此类问题的方法有两种:

  1. 手动同步 Blackboard Learn 应用程序服务器和 ADFS 服务器的时钟。对于 Blackboard Learn,可通过将 /webapps/portal/healthCheck 添加到 Blackboard Learn URL 末尾,在 Web 浏览器中查看服务器的当前时间和时区。

    示例:https://mhtest1.blackboard.com//webapps/portal/healthCheck

    Hostname: ip-10-145-49-11.ec2.internal
    Status: Active - Database connectivity established
    Running since: Sat, Dec 3, 2016 - 05:39:11 PM EST
    Time of request: Thu, Dec 8, 2016 - 05:12:43 PM EST

    机构可以使用上述 URL 来对比 Blackboard Learn 系统与其 ADFS 服务器的时区和时钟,然后根据需要在 ADFS 服务器上调整此类项目,以便其与 Blackboard Learn 站点保持同步。

第 4 个问题

在 ADFS 登录页面输入登录凭证后,系统可能会在重定向到 Blackboard Learn GUI 后显示错误:找不到指定的资源,或者您无权访问该资源登录错误!

bb-services 日志中会显示以下异常:

2016-11-01 12:47:19 -0500 - unsuccessfulAuthentication - org.springframework.security.authentication.AuthenticationServiceException: Error validating SAML message
    at org.springframework.security.saml.SAMLAuthenticationProvider.authenticate(SAMLAuthenticationProvider.java:100)
    at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:167)
    at org.springframework.security.saml.SAMLProcessingFilter.attemptAuthentication(SAMLProcessingFilter.java:87)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:217)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:184)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:64)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:53)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:91)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:176)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262)
    at sun.reflect.GeneratedMethodAccessor929.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.apache.catalina.security.SecurityUtil$1.run(SecurityUtil.java:282)
    at org.apache.catalina.security.SecurityUtil$1.run(SecurityUtil.java:279)
    at java.security.AccessController.doPrivileged(Native Method)
    at javax.security.auth.Subject.doAsPrivileged(Subject.java:549)
    at org.apache.catalina.security.SecurityUtil.execute(SecurityUtil.java:314)
    at org.apache.catalina.security.SecurityUtil.doAsPrivilege(SecurityUtil.java:253)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:190)
    at org.apache.catalina.core.ApplicationFilterChain.access$000(ApplicationFilterChain.java:46)
    at org.apache.catalina.core.ApplicationFilterChain$1.run(ApplicationFilterChain.java:148)
    at org.apache.catalina.core.ApplicationFilterChain$1.run(ApplicationFilterChain.java:144)
    at java.security.AccessController.doPrivileged(Native Method)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:143)
    at blackboard.auth.provider.saml.customization.filter.BbSAMLExceptionHandleFilter.doFilterInternal(BbSAMLExceptionHandleFilter.java:30)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
 [SNIP]
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:745)
Caused by: org.opensaml.common.SAMLException: Response has invalid status code urn:oasis:names:tc:SAML:2.0:status:Responder, status message is null
    at org.springframework.security.saml.websso.WebSSOProfileConsumerImpl.processAuthenticationResponse(WebSSOProfileConsumerImpl.java:113)
    at blackboard.auth.provider.saml.customization.consumer.BbSAMLWebSSOProfileConsumerImpl.processAuthenticationResponse(BbSAMLWebSSOProfileConsumerImpl.java:40)
    at org.springframework.security.saml.SAMLAuthenticationProvider.authenticate(SAMLAuthenticationProvider.java:87)
        ... 230 more
 2016-11-01 12:47:19 -0500 - BbSAMLExceptionHandleFilter - javax.servlet.ServletException: Unsuccessful Authentication
         at blackboard.auth.provider.saml.customization.filter.BbSAMLProcessingFilter.unsuccessfulAuthentication(BbSAMLProcessingFilter.java:31)
         at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:235)
         at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
         at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
         at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:184)
         at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
         at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:64)
         at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
         at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
         at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:53)
         at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
         at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
         at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:91)
         at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
         at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
         at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:176)
         at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
         at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262)
         at sun.reflect.GeneratedMethodAccessor929.invoke(Unknown Source)
         at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
         at java.lang.reflect.Method.invoke(Method.java:498)
         at org.apache.catalina.security.SecurityUtil$1.run(SecurityUtil.java:282)
         at org.apache.catalina.security.SecurityUtil$1.run(SecurityUtil.java:279)
         at java.security.AccessController.doPrivileged(Native Method)
         at javax.security.auth.Subject.doAsPrivileged(Subject.java:549)
         at org.apache.catalina.security.SecurityUtil.execute(SecurityUtil.java:314)
         at org.apache.catalina.security.SecurityUtil.doAsPrivilege(SecurityUtil.java:253)
         at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:190)
         at org.apache.catalina.core.ApplicationFilterChain.access$000(ApplicationFilterChain.java:46)
         at org.apache.catalina.core.ApplicationFilterChain$1.run(ApplicationFilterChain.java:148)
         at org.apache.catalina.core.ApplicationFilterChain$1.run(ApplicationFilterChain.java:144)
         at java.security.AccessController.doPrivileged(Native Method)
         at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:143)
         at blackboard.auth.provider.saml.customization.filter.BbSAMLExceptionHandleFilter.doFilterInternal(BbSAMLExceptionHandleFilter.java:30)
 [SNIP]

解决方法
  1. 导航至“管理员面板”。
  2. 集成下,选择 Building Block
  3. 选择已安装的工具
  4. 在列表中找到验证提供程序 - SAML。打开菜单并选择设置
  5. 在“签名算法设置”下方,选择列表中的 SHA-256。选择签名算法类型后,重新启动 SAML Building Block 以应用新设置。
  6. 选择提交以保存更改。

第 5 个问题

在 ADFS 登录页面输入登录凭证后,系统可能会在重定向到 Blackboard Learn GUI 后显示错误:找不到指定的资源,或者您无权访问该资源登录错误!

bb-services 日志中会显示以下异常:

2017-01-04 22:52:58 -0700 - unsuccessfulAuthentication - org.springframework.security.authentication.AuthenticationServiceException: Error validating SAML message
    at org.springframework.security.saml.SAMLAuthenticationProvider.authenticate(SAMLAuthenticationProvider.java:100)
    at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:167)
    at org.springframework.security.saml.SAMLProcessingFilter.attemptAuthentication(SAMLProcessingFilter.java:87)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:217)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:184)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:64)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:53)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:91)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:176)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262)
    at sun.reflect.GeneratedMethodAccessor935.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.apache.catalina.security.SecurityUtil$1.run(SecurityUtil.java:282)
    at org.apache.catalina.security.SecurityUtil$1.run(SecurityUtil.java:279)
    at java.security.AccessController.doPrivileged(Native Method)
    at javax.security.auth.Subject.doAsPrivileged(Subject.java:549)
    at org.apache.catalina.security.SecurityUtil.execute(SecurityUtil.java:314)
    at org.apache.catalina.security.SecurityUtil.doAsPrivilege(SecurityUtil.java:253)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:190)
    at org.apache.catalina.core.ApplicationFilterChain.access$000(ApplicationFilterChain.java:46)
    at org.apache.catalina.core.ApplicationFilterChain$1.run(ApplicationFilterChain.java:148)
    at org.apache.catalina.core.ApplicationFilterChain$1.run(ApplicationFilterChain.java:144)
    at java.security.AccessController.doPrivileged(Native Method)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:143)
    at blackboard.auth.provider.saml.customization.filter.BbSAMLExceptionHandleFilter.doFilterInternal(BbSAMLExceptionHandleFilter.java:30)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    [SNIP]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:745)
Caused by: org.opensaml.common.SAMLException: NameID element must be present as part of the Subject in the Response message, please enable it in the IDP configuration
    at org.springframework.security.saml.websso.WebSSOProfileConsumerImpl.processAuthenticationResponse(WebSSOProfileConsumerImpl.java:252)
    at blackboard.auth.provider.saml.customization.consumer.BbSAMLWebSSOProfileConsumerImpl.processAuthenticationResponse(BbSAMLWebSSOProfileConsumerImpl.java:40)
    at org.springframework.security.saml.SAMLAuthenticationProvider.authenticate(SAMLAuthenticationProvider.java:87)
        ... 214 more

如上文 SAML 异常中所述,回复消息的主题缺少 NameID 元素。如果未在机构 ADFS IdP 信赖方信任声明规则中将 NameID 设置为传出声明类型,或者 NameID 的声明规则顺序不是机构 ADFS IdP 信赖方信任的正确顺序(这会导致“回复”消息的主题缺少 NameID 元素),则通常会发生此问题。

示例:NameID 元素缺失

<Subject>
    <SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
        <SubjectConfirmationData InResponseTo="a22ai8iig0f75ae22hd28748b12da50"
                                 NotOnOrAfter="2017-01-03T05:57:58.234Z"
                                 Recipient="https://yourschool.blackboard.com/auth-saml/saml/SSO"
                                 />
    </SubjectConfirmation>
</Subject>

示例:NameID 元素存在

<Subject>
    <NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">testadfs</NameID>
    <SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
        <SubjectConfirmationData InResponseTo="a5903d39if463ea87ieiab5135j9ji"
                                 NotOnOrAfter="2017-01-05T04:33:12.715Z"
                                 Recipient="https://yourschool.blackboard.com/auth-saml/saml/SSO"
                                 />
    </SubjectConfirmation>
</Subject>

您可以使用 Firefox SAML 跟踪器附加组件来查看回复消息的“Subject”。

解决方法

解决此类问题的方法有三种。

  1. 确认是否正确遵循了 ADFS SAML B2 设置指南中的步骤,并根据需要做出更改,以转换 ADFS IdP“信赖方信任”的传入声明:
    1. 选择编辑声明规则
    2. 选择添加规则
    3. 选择规则模板页面上,为声明规则模板选择转换传入声明,然后选择下一步
    4. 配置规则页面的声明规则名称字段中,键入将电子邮件转换为名称 ID
    5. 传入声明类型应为 SamAccountName(其必须与最初在将用户名转换为 NameID 规则中创建的传出声明类型匹配)。
    6. 传出声明类型名称 ID
    7. 传出名称 ID 格式为电子邮件
    8. 确认已选择传递所有声明值,然后选择完成
    9. 选择确定以保存规则,再选择一次确定以完成属性映射。
  2. 确保为 ADFS IdP 使用的声明规则(即含有 NameID 元素的规则)顺序在这之前没有任何可选规则。
  3. 如果使用自定义属性,请确保 NameID 元素位于信赖方信任中,因为 Learn 仍期望其 ADFS IdP 发布 NameID 值。

第 6 个问题

通过 SAML 验证登录到 Blackboard Learn 时,用户尝试单击页面左侧的注销按钮来注销,然后单击结束 SSO 会话按钮后,系统立即显示登录错误!

登录错误!
Blackboard Learn 目前无法使用单点登录来登录到您的帐户。请与您的管理员联系寻求帮助。
错误 ID 是 [错误 ID](仅供参考)。

bb-services 日志中会显示以下异常:

2017-05-08 15:10:46 -0400 - BbSAMLExceptionHandleFilter Error Id: f3299757-8d4e-4fab-98cf-49cd99f4891e - javax.servlet.ServletException: Incoming SAML message failed security validation
    at org.springframework.security.saml.SAMLLogoutProcessingFilter.processLogout(SAMLLogoutProcessingFilter.java:145)
    at org.springframework.security.saml.SAMLLogoutProcessingFilter.doFilter(SAMLLogoutProcessingFilter.java:104)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:184)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:64)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:53)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:91)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:176)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
    [SNIP]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:745)
Caused by: org.opensaml.ws.security.SecurityPolicyException: Validation of request simple signature failed for context issuer
    at org.opensaml.common.binding.security.BaseSAMLSimpleSignatureSecurityPolicyRule.doEvaluate(BaseSAMLSimpleSignatureSecurityPolicyRule.java:139)
    at org.opensaml.common.binding.security.BaseSAMLSimpleSignatureSecurityPolicyRule.evaluate(BaseSAMLSimpleSignatureSecurityPolicyRule.java:103)
    at org.opensaml.ws.security.provider.BasicSecurityPolicy.evaluate(BasicSecurityPolicy.java:51)
    at org.opensaml.ws.message.decoder.BaseMessageDecoder.processSecurityPolicy(BaseMessageDecoder.java:132)
    at org.opensaml.ws.message.decoder.BaseMessageDecoder.decode(BaseMessageDecoder.java:83)
    at org.opensaml.saml2.binding.decoding.BaseSAML2MessageDecoder.decode(BaseSAML2MessageDecoder.java:70)
    at org.springframework.security.saml.processor.SAMLProcessorImpl.retrieveMessage(SAMLProcessorImpl.java:105)
    at org.springframework.security.saml.processor.SAMLProcessorImpl.retrieveMessage(SAMLProcessorImpl.java:172)
    at org.springframework.security.saml.SAMLLogoutProcessingFilter.processLogout(SAMLLogoutProcessingFilter.java:131)
    ... 244 more

之所以发生此错误是因为“SAML 设置”页面上的单点注销服务类型设置。

解决方法

需要在 Blackboard Learn 和 ADFS 服务器中配置此设置。

对于作为 IdP 的 ADFS,仅选择发布设置,并在 ADFS 服务器上删除 Learn 实例“信赖方信任”的“重新定向”终结点。

  1. 在 Learn 中,导航到管理员 > 验证 >(提供程序名称)> SAML 设置 > 单点注销服务类型
  2. 选择发布并清除重新定向复选框。
  3. 在 ADFS 服务器中,转到 Learn 实例的“信赖方信任”。
  4. 依次选择属性 > 终结点。系统会列出两个 SAML 注销终结点。
  5. 删除重新定向终结点。选择删除终结点以将其删除,然后依次单击“应用”和“确定”。

在 Learn 和 ADFS 服务器中做出上述更改后,结束 SSO 会话注销按钮会正常运作,供用户执行注销操作。

第 7 个问题

在 ADFS 登录页面输入登录凭证后,系统会在重新定向到 Learn 时显示登录错误!消息。

bb-services 日志中会显示以下 SAML 异常:

2017-05-26 07:39:30 -0400 - unsuccessfulAuthentication - org.springframework.security.authentication.AuthenticationServiceException: Error validating SAML message
        at org.springframework.security.saml.SAMLAuthenticationProvider.authenticate(SAMLAuthenticationProvider.java:100)
        at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:167)
        at org.springframework.security.saml.SAMLProcessingFilter.attemptAuthentication(SAMLProcessingFilter.java:87)
        at blackboard.auth.provider.saml.customization.filter.BbSAMLProcessingFilter.attemptAuthentication(BbSAMLProcessingFilter.java:46)
        at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:217)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
        at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
        at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:184)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
        at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:64)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
        at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:53)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
        at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:91)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
        at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
        at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:176)
        at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
        at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262)
        at sun.reflect.GeneratedMethodAccessor380.invoke(Unknown Source)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at org.apache.catalina.security.SecurityUtil$1.run(SecurityUtil.java:282)
        at org.apache.catalina.security.SecurityUtil$1.run(SecurityUtil.java:279)
        at java.security.AccessController.doPrivileged(Native Method)
        at javax.security.auth.Subject.doAsPrivileged(Subject.java:549)
        at org.apache.catalina.security.SecurityUtil.execute(SecurityUtil.java:314)
        at org.apache.catalina.security.SecurityUtil.doAsPrivilege(SecurityUtil.java:253)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:190)
        at org.apache.catalina.core.ApplicationFilterChain.access$000(ApplicationFilterChain.java:46)
        at org.apache.catalina.core.ApplicationFilterChain$1.run(ApplicationFilterChain.java:148)
        at org.apache.catalina.core.ApplicationFilterChain$1.run(ApplicationFilterChain.java:144)
        at java.security.AccessController.doPrivileged(Native Method)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:143)
        at blackboard.auth.provider.saml.customization.filter.BbSAMLExceptionHandleFilter.doFilterInternal(BbSAMLExceptionHandleFilter.java:37)
    [SNIP]
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
        at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
        at java.lang.Thread.run(Thread.java:745)
Caused by: org.opensaml.common.SAMLException: Response has invalid status code urn:oasis:names:tc:SAML:2.0:status:Responder, status message is null
        at org.springframework.security.saml.websso.WebSSOProfileConsumerImpl.processAuthenticationResponse(WebSSOProfileConsumerImpl.java:113)
        at blackboard.auth.provider.saml.customization.consumer.BbSAMLWebSSOProfileConsumerImpl.processAuthenticationResponse(BbSAMLWebSSOProfileConsumerImpl.java:56)
        at org.springframework.security.saml.SAMLAuthenticationProvider.authenticate(SAMLAuthenticationProvider.java:87)
        ... 247 more

解决方法

从 Blackboard Learn 3200.0.0 开始,我们现在提供用于重新生成 SAML 加密证书的选项(导航到系统管理员 > Building Block > 验证提供程序 - SAML > 设置 > 重新生成证书便可找到)。如果在 ADFS 服务器中已将 SP 元数据上传到 Learn 站点的“依赖方信任”之后单击重新生成证书按钮,则可能会发生登录错误!问题。要解决此问题,请执行以下操作:

  1. 导航到系统管理员 > 验证 > [SAML 提供程序名称] > SAML 设置
  2. 选择“服务提供程序元数据”旁边的生成,以保存新的元数据文件。
  3. 访问 ADFS 服务器并将新的 SP 元数据上传到 Learn 站点的“信赖方信任”。

如果您在 B2 设置下生成新证书,则需要将 SAML B2 切换为非活动,然后返回活动以强制进行更改。之后,您可以返回到提供程序设置并生成新的元数据以导入到 IDP 中。如果您不切换设置,则在生成新的元数据时,可能仍会包含旧证书。IDP 不会更新,下次 Learn 重新启动时,它将显示新证书。由于此不匹配,SAML 身份验证将中断。

联合元数据

使用 Active Directory 联合身份验证服务 (ADFS) 时,由于通常位于 https://[ADFS Server Hostname]/FederationMetadata/2007-06/FederationMetadata.xml 的 ADFS 联合元数据包含与 SAML 2.0 不兼容的元素,因此,必须对此类元数据编辑以删除不兼容的元素,然后才能将其上传到 Blackboard Learn GUI SAML 验证设置页面的身份验证提供程序设置部分。如果上传含有不兼容元素的元数据,则会在选择 Blackboard Learn 登录页面上的 SAML 登录链接时发生错误:找不到实体 [entity] 和角色 {} 的元数据。错误 ID 是 [error ID](仅供参考)。

bb-services 日志中错误 ID 对应的 Java 堆栈跟踪如下所示:

2016-06-21 11:42:51 -0700 - Metadata for entity https://<Learn Server Hostname>/adfs/ls/ and role {urn:oasis:names:tc:SAML:2.0:metadata}SPSSODescriptor wasn&#39;t found<P><span class="captionText">For reference, the Error ID is c99511ae-1162-4941-b823-3dda19fea157.</span> - org.opensaml.saml2.metadata.provider.MetadataProviderException: Metadata for entity https://ulvsso.laverne.edu/adfs/ls/ and role {urn:oasis:names:tc:SAML:2.0:metadata}SPSSODescriptor wasn't found
    at org.springframework.security.saml.context.SAMLContextProviderImpl.populateLocalEntity(SAMLContextProviderImpl.java:319)
    at org.springframework.security.saml.context.SAMLContextProviderImpl.populateLocalContext(SAMLContextProviderImpl.java:216)
    at org.springframework.security.saml.context.SAMLContextProviderImpl.getLocalAndPeerEntity(SAMLContextProviderImpl.java:126)
    at org.springframework.security.saml.SAMLEntryPoint.commence(SAMLEntryPoint.java:146)
    at org.springframework.security.saml.SAMLEntryPoint.doFilter(SAMLEntryPoint.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:184)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:64)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:53)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:91)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:176)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262)
    at sun.reflect.GeneratedMethodAccessor1652.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
        [SNIP]

解决方法

由于 ADFS 联合的默认元数据位置为 https://[ADFS server hostname]/FederationMetadata/2007-06/FederationMetadata.xml

  1. 下载此文件并在文本编辑器中将其打开。请谨慎删除以 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"> ... </X509Data></KeyInfo> 开始且以 </ds:Signature>
    <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"> 结束的部分

    <ds:SignedInfo>
      <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
      <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
      <ds:Reference URI="#_43879f32-9a91-4862-bc87-e98b85b51158">
       <ds:Transforms>
        <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
        <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
       </ds:Transforms>
       <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
       <ds:DigestValue>z1H1[SNIP]jaYM=</ds:DigestValue>
      </ds:Reference>
      </ds:SignedInfo>
      <ds:SignatureValue> FVj[SNIP]edrfNKWvsvk5A==
      </ds:SignatureValue>
      <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
       <X509Data>
        <X509Certificate>
        FDdd[SNIP]qTNKdk5F/vf1AocDaX
        </X509Certificate>
       </X509Data>
      </KeyInfo>
    </ds:Signature>

  2. 将更新后的元数据 XML 文件上传到 Blackboard Learn GUI 中 SAML 身份验证设置页面的身份验证提供程序设置部分。
  3. 切换 SAML 身份验证提供程序和 SAML B2 非活动/可用,同时使 SAML 身份验证提供程序处于“活动”状态。

如果机构在 Blackboard Learn 站点上测试 SAML 身份验证,且其多个 SAML 身份验证提供程序在 Blackboard Learn 站点上共用同一个基本 ADFS IdP 元数据 XML 文件,则即使将其他 SAML 身份验证提供程序设置为“非活动”,也需要在 Blackboard Learn GUI “SAML 身份验证设置”页面的“身份验证提供程序设置”部分为此类验证提供程序上传更新后的元数据 XML 文件。然后,应将 SAML B2 切换为“非活动/可用”,同时使 SAML 身份验证提供程序处于“活动”状态,以确保更新后的元数据 XML 文件可在系统范围内得到识别。

错误的用户查找方法

在 ADFS 登录页面输入登录凭证后,系统会将用户重新定向到 Blackboard Learn GUI,但并未让其登录到 Blackboard Learn。

bb-services 日志会记录仅与 SAML 验证相关的事件,如下所示:

2016-10-18 13:03:28 -0600 - userName is null or empty

解决方法

  1. 以管理员身份使用默认 Blackboard Learn 内部验证登录到 Blackboard Learn。
  2. 导航到系统管理员 >“SAML 验证提供程序名称”> 编辑
  3. 用户查找方法批处理 UID 更改为用户名

额外的“结束 SSO 会话”注销按钮

首次选择 Blackboard Learn GUI 右上方的注销按钮后,ADFS 会尝试在显示的结束所有会话?页面上添加额外的结束 SSO 会话注销按钮。

要实现这一点,可向 IdP 元数据文件中添加额外的 SingleLogoutService,如下所示:

<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://your.server.name/adfs/ls/"/>
<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://your.server.name/adfs/ls/"/>

由于这是一项可选 SAML B2 IdP 配置,且在重新定向端点中提供的签名不正确,因此,在选择结束所有会话?页面上额外的结束 SSO 会话按钮时将发生错误:传入 SAML 消息的安全性验证失败。针对上下文发布者发出的请求简单签名验证失败。错误 ID 是 [错误 ID](仅供参考)。

bb-services 日志中错误 ID 对应的 Java 堆栈跟踪如下所示:

2016-10-17 16:57:44 -0400 - Incoming SAML message failed security validation Validation of request simple signature failed for context issuer<P><span class="captionText">For reference, the Error ID is 930c7767-8710-475e-8415-2077152280e0.</span> - org.opensaml.ws.security.SecurityPolicyException: Validation of request simple signature failed for context issuer
    at org.opensaml.common.binding.security.BaseSAMLSimpleSignatureSecurityPolicyRule.doEvaluate(BaseSAMLSimpleSignatureSecurityPolicyRule.java:139)
    at org.opensaml.common.binding.security.BaseSAMLSimpleSignatureSecurityPolicyRule.evaluate(BaseSAMLSimpleSignatureSecurityPolicyRule.java:103)
    at org.opensaml.ws.security.provider.BasicSecurityPolicy.evaluate(BasicSecurityPolicy.java:51)
    at org.opensaml.ws.message.decoder.BaseMessageDecoder.processSecurityPolicy(BaseMessageDecoder.java:132)
    at org.opensaml.ws.message.decoder.BaseMessageDecoder.decode(BaseMessageDecoder.java:83)
    at org.opensaml.saml2.binding.decoding.BaseSAML2MessageDecoder.decode(BaseSAML2MessageDecoder.java:70)
    at org.springframework.security.saml.processor.SAMLProcessorImpl.retrieveMessage(SAMLProcessorImpl.java:105)
    at org.springframework.security.saml.processor.SAMLProcessorImpl.retrieveMessage(SAMLProcessorImpl.java:172)
    at org.springframework.security.saml.SAMLLogoutProcessingFilter.processLogout(SAMLLogoutProcessingFilter.java:131)
    at org.springframework.security.saml.SAMLLogoutProcessingFilter.doFilter(SAMLLogoutProcessingFilter.java:104)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:184)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:64)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:53)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:91)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:176)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262)
    at sun.reflect.GeneratedMethodAccessor1652.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
        [SNIP]

解决方法

  1. 访问 ADFS 服务器并转到 Blackboard Learn 实例的信赖方信任
  2. 依次选择属性 > 终结点标签。
  3. 终结点标签会列出 2 个 SAML 注销终结点
  4. 删除重新定向终结点。
  5. 选择删除终结点以将其删除,然后依次单击应用确定

删除重新定向终结点后,结束 SSO 会话按钮会正常运作,供用户执行注销操作。

通过事件查看器查看应用程序日志

排除 ADFS SAML 验证问题时,可能还需要让机构在其 ADFS 服务器中通过事件查看器查看 ADFS 应用程序日志,以获取更深入的分析。当 ADFS 服务器的 SAML 响应状态为请求遭拒(如下所示)时,这种做法非常必要:

<samlp:Status>
    <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Responder">
        <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:RequestDenied" />
    </samlp:StatusCode>
</samlp:Status>

SAML 响应可以使用 Firefox 浏览器 SAML 跟踪器附加组件进行查看。

响应的请求遭拒状态通常表示在 IdP (ADFS) 尝试理解响应并处理 SP (Blackboard Learn) 提供的结果时所发生的问题。

要通过事件查看器查看 ADFS 应用程序日志,请执行以下操作:

  1. 在 ADFS 服务器上打开事件查看器
  2. 视图菜单中,选择显示分析和调试日志
  3. 在控制台树中,导航到应用程序和服务日志 > ADFS 跟踪 > 调试

Azure Active Directory

Azure AD 是 Microsoft (MS) 基于云的目录和身份管理服务。

发送电子邮件的第一部分

如果机构使用 Azure AD 作为自己的 IdP,并希望仅将 Azure AD 电子邮件用户名的第一部分用作 Blackboard Learn 用户名,则可以配置 Azure AD IdP,以使用特殊 ExtractMailPrefix() 功能从电子邮件或用户主体名称中删除域后缀,从而只形成传递的用户名的第一部分(如“joesmith”,而非 joesmith@example.com)。

如果 Blackboard Learn 远程用户 IDurn:oid:1.3.6.1.4.1.5923.1.1.1.6,则 Azure IdP 的属性设置将如下所示:

Attribute Name:        urn:oid:1.3.6.1.4.1.5923.1.1.1.6
Attribute Value:    ExtractMailPrefix()
Mail:            user.userprincipalname

因此,使用 joesmith@example.com 这个示例电子邮件用户名,在 SAML 断言中将其从 Azure IdP 传递到 Blackboard Learn 的过程如下所示:

<Attribute Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.6">
    <AttributeValue>joesmith</AttributeValue>

有关使用 ExtractMailPrefix() 功能的其他信息,请参阅 MS Azure 文档页面

Azure AD IdP 更新证书

在 MS Azure AD 登录页面输入登录凭证后,系统可能会在重新定向到 Blackboard Learn GUI 后显示登录错误!

bb-services 日志中会显示以下异常:

2016-10-13 12:03:23 +0800 - unsuccessfulAuthentication - org.springframework.security.authentication.AuthenticationServiceException: Error validating SAML message
 at org.springframework.security.saml.SAMLAuthenticationProvider.authenticate(SAMLAuthenticationProvider.java:100)
 at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:167)
 at org.springframework.security.saml.SAMLProcessingFilter.attemptAuthentication(SAMLProcessingFilter.java:87)
 at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:217)
 at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
 at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
 at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:184)
 at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
 at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:64)
 at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
 at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
 at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:53)
 at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
 at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
 at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:91)
 at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
 at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
 at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:176)
 at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
 at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262)
 at sun.reflect.GeneratedMethodAccessor854.invoke(Unknown Source)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
 at java.lang.reflect.Method.invoke(Method.java:498)
 at org.apache.catalina.security.SecurityUtil$1.run(SecurityUtil.java:282)
 at org.apache.catalina.security.SecurityUtil$1.run(SecurityUtil.java:279)
 at java.security.AccessController.doPrivileged(Native Method)
 at javax.security.auth.Subject.doAsPrivileged(Subject.java:549)
 at org.apache.catalina.security.SecurityUtil.execute(SecurityUtil.java:314)
 at org.apache.catalina.security.SecurityUtil.doAsPrivilege(SecurityUtil.java:253)
 at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:190)
 at org.apache.catalina.core.ApplicationFilterChain.access$000(ApplicationFilterChain.java:46)
 at org.apache.catalina.core.ApplicationFilterChain$1.run(ApplicationFilterChain.java:148)
 at org.apache.catalina.core.ApplicationFilterChain$1.run(ApplicationFilterChain.java:144)
 at java.security.AccessController.doPrivileged(Native Method)
 at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:143)
 at blackboard.auth.provider.saml.customization.filter.BbSAMLExceptionHandleFilter.doFilterInternal(BbSAMLExceptionHandleFilter.java:30)
 [SNIP]
 at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
 at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
 at java.lang.Thread.run(Thread.java:745)
Caused by: org.opensaml.common.SAMLException: Response doesn't have any valid assertion which would pass subject validation
 at org.springframework.security.saml.websso.WebSSOProfileConsumerImpl.processAuthenticationResponse(WebSSOProfileConsumerImpl.java:229)
 at blackboard.auth.provider.saml.customization.consumer.BbSAMLWebSSOProfileConsumerImpl.processAuthenticationResponse(BbSAMLWebSSOProfileConsumerImpl.java:40)
 at org.springframework.security.saml.SAMLAuthenticationProvider.authenticate(SAMLAuthenticationProvider.java:87)
 ... 230 more
Caused by: org.opensaml.xml.validation.ValidationException: Signature is not trusted or invalid
 at org.springframework.security.saml.websso.AbstractProfileBase.verifySignature(AbstractProfileBase.java:272)
 at org.springframework.security.saml.websso.WebSSOProfileConsumerImpl.verifyAssertionSignature(WebSSOProfileConsumerImpl.java:419)
 at org.springframework.security.saml.websso.WebSSOProfileConsumerImpl.verifyAssertion(WebSSOProfileConsumerImpl.java:292)
 at org.springframework.security.saml.websso.WebSSOProfileConsumerImpl.processAuthenticationResponse(WebSSOProfileConsumerImpl.java:214)
 ... 232 more

这是由 MS Azure AD IdP 更新证书引起的,但 Blackboard Learn SP 使用的元数据 XML 未经过调整以反映新证书。

解决方法

  • 需要在 Blackboard Learn GUI 上身份验证提供程序的 SAML 设置页面上更新新的元数据 XML 文件和新证书。
  • 然后,需要将 SAML B2 和身份验证提供程序切换为“非活动/可用”,同时使 SAML 身份验证提供程序处于“活动”状态,以应用更新后的元数据和新证书。
  • 如果 Blackboard Learn 站点的多个身份验证提供程序为相同的基本 IdP 实体 ID 使用同一个基本证书,则所有这些身份验证提供程序都需要更新。

Microsoft 表示,从现在起将会每 6 周更新一次证书,但不会向外界宣布此类更新。


IdP 发起的单点登录

如果用户首次登录用户门户并为 Blackboard Learn 站点选择应用程序,则系统会打开新浏览器标签来显示消息:找不到指定的资源,或者您无权访问该资源

stdout-stderr.log 中与 SAML 相关的对应事件如下所示:

INFO   | jvm 1    | 2016/08/16 10:49:22 | - /saml/SSO at position 1 of 10 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter'
INFO   | jvm 1    | 2016/08/16 10:49:22 | - HttpSession returned null object for SPRING_SECURITY_CONTEXT
INFO   | jvm 1    | 2016/08/16 10:49:22 | - No SecurityContext was available from the HttpSession: org.apache.catalina.session.StandardSessionFacade@58c53845. 将创建一个新项。
INFO   | jvm 1    | 2016/08/16 10:49:22 | - /saml/SSO at position 2 of 10 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter'
INFO   | jvm 1    | 2016/08/16 10:49:22 | - /saml/SSO at position 3 of 10 in additional filter chain; firing Filter: 'HeaderWriterFilter'
INFO   | jvm 1    | 2016/08/16 10:49:22 | - /saml/SSO at position 4 of 10 in additional filter chain; firing Filter: 'FilterChainProxy'
INFO   | jvm 1    | 2016/08/16 10:49:22 | - Checking match of request : '/saml/sso'; against '/saml/login/**'
INFO   | jvm 1    | 2016/08/16 10:49:22 | - Checking match of request : '/saml/sso'; against '/saml/logout/**'
INFO   | jvm 1    | 2016/08/16 10:49:22 | - Checking match of request : '/saml/sso'; against '/saml/bbsamllogout/**'
INFO   | jvm 1    | 2016/08/16 10:49:22 | - Checking match of request : '/saml/sso'; against '/saml/sso/**'
INFO   | jvm 1    | 2016/08/16 10:49:22 | - /saml/SSO at position 1 of 1 in additional filter chain; firing Filter: 'SAMLProcessingFilter'
INFO   | jvm 1    | 2016/08/16 10:49:22 | - Forwarding to /
INFO   | jvm 1    | 2016/08/16 10:49:22 | - DispatcherServlet with name 'saml' processing POST request for [/auth-saml/saml/SSO]
INFO   | jvm 1    | 2016/08/16 10:49:22 | - No mapping found for HTTP request with URI [/auth-saml/saml/SSO] in DispatcherServlet with name 'saml'
INFO   | jvm 1    | 2016/08/16 10:49:22 | - SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.
INFO   | jvm 1    | 2016/08/16 10:49:22 | - Successfully completed request
INFO   | jvm 1    | 2016/08/16 10:49:22 | - Skip invoking on
INFO   | jvm 1    | 2016/08/16 10:49:22 | - SecurityContextHolder now cleared, as request processing completed

SAML 验证设置页面的服务提供程序设置部分发生了变化,且应选中启用自动 SSO 选项,以允许用户通过门户访问 Blackboard Learn。如果启用该选项,则 ACS URL 还会更改为包括别名。


错误文档错误

在 SAML 验证提供程序登录页面输入登录凭证后,系统可能会在重新定向到 Blackboard Learn GUI 后显示登录错误!

bb-services 日志中会显示以下 DOMExceptionWRONG_DOCUMENT_ERR

2016-11-18 12:27:31 -0600 - WRONG_DOCUMENT_ERR: A node is used in a different document than the one that created it.<P><span class="captionText">For reference, the Error ID is 86ebb81d-d3a3-4da5-95ab-1c94505f4281.</span> - org.w3c.dom.DOMException: WRONG_DOCUMENT_ERR: A node is used in a different document than the one that created it.
    at org.apache.xerces.dom.ParentNode.internalInsertBefore(Unknown Source)
    at org.apache.xerces.dom.ParentNode.insertBefore(Unknown Source)
    at org.apache.xerces.dom.NodeImpl.appendChild(Unknown Source)
    at org.opensaml.xml.encryption.Decrypter.parseInputStream(Decrypter.java:832)
    at org.opensaml.xml.encryption.Decrypter.decryptDataToDOM(Decrypter.java:610)
    at org.opensaml.xml.encryption.Decrypter.decryptUsingResolvedEncryptedKey(Decrypter.java:795)
    at org.opensaml.xml.encryption.Decrypter.decryptDataToDOM(Decrypter.java:535)
    at org.opensaml.xml.encryption.Decrypter.decryptDataToList(Decrypter.java:453)
    at org.opensaml.xml.encryption.Decrypter.decryptData(Decrypter.java:414)
    at org.opensaml.saml2.encryption.Decrypter.decryptData(Decrypter.java:141)
    at org.opensaml.saml2.encryption.Decrypter.decrypt(Decrypter.java:69)
    at org.springframework.security.saml.websso.WebSSOProfileConsumerImpl.processAuthenticationResponse(WebSSOProfileConsumerImpl.java:199)
    at org.springframework.security.saml.SAMLAuthenticationProvider.authenticate(SAMLAuthenticationProvider.java:82)
    at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:167)
    at org.springframework.security.saml.SAMLProcessingFilter.attemptAuthentication(SAMLProcessingFilter.java:87)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:217)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:184)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:64)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:53)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:91)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:176)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262)
    at sun.reflect.GeneratedMethodAccessor1209.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.apache.catalina.security.SecurityUtil$1.run(SecurityUtil.java:277)
    at org.apache.catalina.security.SecurityUtil$1.run(SecurityUtil.java:274)
    at java.security.AccessController.doPrivileged(Native Method)
    at javax.security.auth.Subject.doAsPrivileged(Subject.java:549)
    at org.apache.catalina.security.SecurityUtil.execute(SecurityUtil.java:309)
    at org.apache.catalina.security.SecurityUtil.doAsPrivilege(SecurityUtil.java:249)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:237)
    at org.apache.catalina.core.ApplicationFilterChain.access$000(ApplicationFilterChain.java:55)
    at org.apache.catalina.core.ApplicationFilterChain$1.run(ApplicationFilterChain.java:191)
    at org.apache.catalina.core.ApplicationFilterChain$1.run(ApplicationFilterChain.java:187)
    at java.security.AccessController.doPrivileged(Native Method)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:186)
    at blackboard.platform.servlet.DevNonceFilter.doFilter(DevNonceFilter.java:68)
    [SNIP]

出现该问题的原因是另一个B2/Project 将系统属性 javax.xml.parsers.DocumentBuilderFactory 值从 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl 更改为 com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl.

临时解决方法

在发布修复之前,临时解决方法如下:

  1. 在每个节点上重新启动 Bb 服务。
  2. 关闭 IdP 端的 SAML 响应加密。由于整个通信都是通过 SSL 进行的,因此这不会降低验证的安全性。

用于向“提供程序顺序”添加 SAML 的选项不存在

配置 SAML 验证时,机构可能会发现,在 Blackboard Learn GUI 的提供程序顺序部分中用于添加 SAML 验证提供程序的选项不存在(导航到系统管理员 > Building Block:验证 > 提供程序顺序后)。

用于向提供程序顺序添加 SAML 验证提供程序的选项不存在的原因是,重新定向类型的提供程序(如 CAS 和 SAML)将验证传递给远程验证源。提供程序顺序未列出此类提供程序,因为其被视为验证的权威源并处理自己的验证故障。


测试 SAML 连接

从 Blackboard Learn 2016 年第四季度版本开始,Blackboard Learn GUI 的验证部分提供用于测试 SAML 提供程序连接的选项。连接测试会检查以下项目:

  • 解析 IdP 元数据
  • 与 IdP 的连接
  • 接收 SAML 响应
  • 解析 SAML 响应
  • 远程用户 ID 匹配
  • 登录到 Blackboard Learn

要测试 SAML 验证提供程序的连接,请执行以下操作:

  1. 以管理员身份登录到 Blackboard Learn。
  2. 导航到系统管理员 > Building Block:验证 >“SAML 提供程序名称”> 测试连接
  3. 输入 IdP 登录凭证(如果系统提示)。

可以使用测试连接功能来代替在 Blackboard Learn 中手动启用 SAML 调试日志记录(因多种原因)。

测试连接输出页面上显示的身份验证提供程序实体 ID 值是在用户经过验证后从 IdP 传递到 Blackboard Learn 的 SAML POST 发布者元素中提取的:

<Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">http://bbpdcsi-adfs1.bbpdcsi.local/a...services/trust</Issuer>

测试连接输出页面上 SAML 响应部分显示的 SAML 属性值是在用户经过验证后从 IdP 传递给 Blackboard Learn 的 SAML POST SubjectAttributeStatement 元素中提取的:

<Subject>
    <NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">luke.skywalker</NameID>
    [SNIP]
</Subject>
<AttributeStatement>
    <Attribute Name="SamAccountName">
        <AttributeValue>luke.skywalker</AttributeValue>
    </Attribute>
    <Attribute Name="urn:oid:2.5.4.42">
        <AttributeValue>Luke</AttributeValue>
    </Attribute>
    <Attribute Name="urn:oid:2.5.4.4">
        <AttributeValue>Skywalker</AttributeValue>
    </Attribute>
</AttributeStatement>


创建 SAML 验证提供程序和 IdP 以进行测试

按照以下步骤,使用 Centrify 的免费 SSO 验证解决方案来创建身份验证提供程序 (IdP)。

然后,在 Blackboard Learn 服务提供程序 (SP) 中将该 IdP 配置为 SAML 验证提供程序:

Blackboard Learn 服务提供程序

  1. 以管理员身份登录到 Blackboard Learn GUI 并导航到系统管理员 > 验证
  2. 依次选择创建提供程序 > SAML
  3. 输入以下设置:
    • 名称 > SAML 或所需的任何内容。
    • 验证提供程序 > 非活动(暂时)。
    • 用户查找方法 > 用户名
    • 受主机名限制 > 将此提供程序用于任何主机名
    • 链接文本 > SAML Centrify 登录
  4. 选择保存并配置
  5. 实体 ID 字段中,将其设为您所需的任何值(但如果您更改了该值,则必须为身份验证提供程序提供更新后的服务提供程序元数据)。只需复制/粘贴 ACS URL 即可。
  6. 在“服务提供程序元数据”下方,选择生成,然后将文件保存到您的桌面。
  7. 数据源下方,建议为此命名的 CENTRIFY 新建一个数据源,否则,请使用“系统”或您所选的任何内容
  8. 选中启用 JIT 配置旁边的方框,这样一来,如果用户不存在,系统会在尝试通过此 SAML 验证提供程序登录时自动创建一个帐户。如果未选中 JIT 配置,则需要手动创建 Blackboard Learn 中的用户。
  9. 兼容数据源列表中,务必选择应与此验证提供程序兼容的数据源。
  10. 身份验证提供程序类型选择单点身份验证提供程序
  11. 暂时跳过身份验证提供程序元数据,Centrify IdP 部分创建完此类元数据后,您便可上传文件。
  12. 对于映射 SAML 属性部分,将 NameID 用于远程用户 ID
  13. 选择“提交”。

Centrify 身份验证提供程序

  1. 转到 Centrify 网站并选择立即开始
  2. 输入您的信息以进行注册,然后选择立即开始
  3. 您会收到一封含有管理凭证的欢迎电子邮件。使用此类凭证登录 https://cloud.centrify.com
  4. 在“欢迎使用 Centrify 身份服务”窗口中选择跳过
  5. 在页面顶部的应用程序标签中,选择添加 Web 应用程序按钮。
  6. 自定义标签中,向下滚动并选择 SAML 对应的添加按钮。选择
  7. 添加 Web 应用程序窗口底部选择关闭
  8. 转到应用程序标签。在应用程序设置部分,选择上传 SP 元数据按钮,然后上传在 Blackboard Learn SP 部分第 6 步中创建的文件。
  9. 上传 SP 元数据后,断言使用者服务 URL 应该会自动填充。
  10. 取消选中加密断言。这样可支持使用 Firefox 浏览器 SAML 跟踪器附加组件或 Chrome SAML Message Decoder 来查看从 IdP 发布并发送给 Blackboard Learn 的属性。由于整个通信都是通过 SSL 进行的,因此这不会降低验证的安全性。
  11. 向下滚动并选择下载身份验证提供程序 SAML 元数据。将文件保存到您的桌面。
  12. 选择保存,然后转到下一个部分。
  13. 描述部分输入一个名称。选择保存,然后转到下一个部分。
  14. 用户访问部分,选择所有人系统管理员。选择保存
  15. 请勿在策略部分选择任何内容。
  16. 对于帐户映射部分,确认为目录服务字段输入了 userprincipalname 名称。
  17. 对于高级部分,在用来为应用程序生成 SAML 断言的脚本底部添加以下行:

    完整的脚本如下所示:

    setIssuer(Issuer);
    setSubjectName(UserIdentifier);
    setAudience('https://YourLearnServer.blackboard.c...saml/saml/SSO');
    setRecipient(ServiceUrl);
    setHttpDestination(ServiceUrl);
    setSignatureType('Assertion');
    setNameFormat('emailaddress');
    setAttribute("NameID", LoginUser.Get("userprincipalname"));

    这样可允许 Centrify IdP 通过 SAML POST 发布包含用户 ID 的 AttributeStatement。

    示例:

    <AttributeStatement>
        <Attribute Name="NameID"
                   NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"
                   >
            <AttributeValue>luke.skywalker@blackboard.com.47</AttributeValue>
        </Attribute>
    </AttributeStatement>

    详细了解如何在 Centrify SAML 脚本中指定断言元素

  18. 选择保存
  19. 无需更改其余部分(应用程序网关更改日志工作流程)。
  20. 应用程序标签中,确认 SAML 应用程序已自动部署。
  21. 用户标签中,选择添加用户,输入用户的帐户信息,然后选择创建用户
  22. 以管理员身份重新登录到 Blackboard Learn GUI,导航到系统管理员 > 验证 > SAML 验证提供程序名称 > SAML 设置 > 身份验证提供程序设置,上传在第 13 步中保存到您桌面的 IdP 元数据文件,然后选择提交

现在,创建的 Centrify IdP 用户可以选择登录页面上的这个验证提供程序通过 SAML 登录到 Blackboard Learn,在选择 Blackboard Learn 右上方的注销按钮后使用结束所有会话?页面上显示的额外结束 SSO 会话注销按钮来注销 Blackboard Learn。


在“结束 SSO 会话”注销页面上更改文本

机构可能会询问是否能在“结束 SSO 会话”注销页面上更改文本。可以通过编辑语言包在“结束 SSO 会话”注销页面上更改文本:

  1. 打开语言包文件。
  2. 导航到 auth-provider-saml/src/main/webapp/WEB-INF/bundles/bb-manifest-en_US.properties
  3. 更新消息密钥:

    saml.single.logout.warning.conent.description // the first line
    saml.single.logout.warning.conent.recommend // second line
    saml.single.logout.warning.endsso.title // third line
    saml.single.logout.warning.endsso.button // the button
    saml.single.logout.warning.backtolearn // the cancel button


将用户重新定向到 IdP 登录页面

标准 Blackboard Learn 登录页面会显示默认 Learn 内部验证提供程序的用户名和密码字段。当您启用 SAML 验证时,SAML 的“使用…登录”小链接会显示在此页面底部,因此,您可能需要在用户访问 Learn 登录页面时自动将用户重新定向到 IdP 的验证服务器。

完成此操作的一个方法是,导航到系统管理员 > 验证并将默认 Learn 内部验证设置为“非活动”,这意味着,登录页面不再显示,系统会立即将用户重新定向到 SAML 登录页面。但使用此方法的问题是,它会覆盖默认登录 URL 并阻止任何非 SAML 用户登录。

为了避免这个问题并提供几乎相同的结果,请使用“定制登录页面”。用户会被重新定向到 SAML 验证提供程序的 IdP 登录页面,但默认登录链接还可供使用。

  1. 请确保默认 Learn 内部验证处于活动状态。
  2. 在默认登录页面上,复制提供程序重新定向的位置,如登录方式… SAML。右键单击该链接并选择复制链接位置
  3. 导航到系统管理员 > 社区 > 主页面和主题 > 定制登录页面
  4. 选择默认登录页面旁边的下载,以下载默认登录 JSP 文件。
  5. 使用文本编辑器打开 JSP文件。在登录 JSP 文件中添加以下示例 HTML,并将 URL 文本替换为在第 2 步中复制的 URL。

    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
    <html>
    <head>
    <title>Blackboard Learn - Redirect</title>
    <meta http-equiv="REFRESH" content="0;url=https://URL_Goes_Here"></HEAD>
    <BODY style="font-family: arial,sans-serif;font-size: small; color: grey; padding: 1em; ">
    Redirecting... <a style="color:grey" href="https://URL_Goes_Here">Go to login page</a> if you are not automatically redirected.
    </BODY>
    </HTML>

  6. 在 Learn 中再次导航到定制登录页面。选择使用自定义页面,然后上传更新后的登录 JSP 文件。
  7. 做出相应更改后,选择定制登录页面上的预览,确认重新定向可以正常运行。

现在,转到主要 URL 的用户可以重新定向到 SAML 验证提供程序的登录页面。管理员仍可以使用 Learn 内部验证通过默认登录页面进行登录:/webapps/login/?action=default_login/webapps/login/login.jsp )。

详细了解在 Ultra 体验中定制登录页面