文章
身份和安全性
作者:Abhijit Patil
Oracle WebLogic Server 为 Microsoft 客户端使用 Kerberos 进行一次性登录提供了全面的解决方案。
2012 年 5 月发布
本文介绍如何让 Microsoft 客户端(在本例中为浏览器)在 Windows 域内使用 Kerberos 进行身份验证之后能够凭借同样的凭证在 Oracle WebLogic Server (Oracle WebLogic Server) 域中透明地通过身份验证,而不必再次键入口令。
该特性旨在让客户端浏览器能够访问 Oracle WebLogic Server 上的受保护资源,并通过 SPNEGO 票证透明地向 Oracle WebLogic Server 提供来自 Kerberos 数据库的身份验证信息。注意,该特性还适用于 Java SE 客户端。Oracle WebLogic Server 将能够识别票证,并从中提取信息。然后该服务器将使用该信息进行身份验证,如果被验证的用户有权访问资源,则允许其访问该资源。(Kerberos 仅负责身份验证;授权仍由 Oracle WebLogic Server 处理。)
以下配置用于演示此场景:
注意,尽管本场景使用以上配置,但 SPNEGO 应适用于更早版本的浏览器、Oracle WebLogic Server、JDK 等。
图 1:SPNEGO/Kerberos 场景的计算机配置
下面的步骤列表详细分解了上图所示跨平台身份验证设计的过程。
Windows 2008 Server 域控制器可以充当基于 Kerberos 的客户端和主机系统的 Kerberos 密钥分发中心 (KDC) 服务器。
在这一步中,在 Active Directory 上创建一个代表 Oracle WebLogic Server 的 Kerberos 主体。主体名称将类似于 name@REALM.NAME,其中 REALM.NAME 是领域的管理名称。在本例中,主体名称将是 negotiatetestserver@SECURITYQA.COM。托管 Oracle WebLogic Server 的计算机不必是 SECURITYQA.com 域的一部分。在本例中,它是 OTHERDOM.DOM 域的一部分。在 AD 中,帐户类型应为“User”而不是“Computer”。

图 2:Account 选项卡,显示 KDC 上“negotiatetestserver”用户的属性
SPN(服务主体名称)是标识一个服务实例的唯一名称,与运行该服务实例的登录帐户关联。SPN 用于客户端与托管特定服务的服务器之间的相互身份验证过程。客户端根据它尝试连接的服务的 SPN 找到计算机帐户。
ktpass 命令行工具允许管理员将非 Windows Server Kerberos 服务配置为 Windows Server Active Directory 中的安全主体。Ktpass 在 Active Directory 中配置该服务的服务器主体名称并生成一个 MIT 式的 Kerberos“keytab”文件,其中包含该服务的共享密钥。该工具允许支持 Kerberos 身份验证的基于 UNIX 的服务使用 Windows Server Kerberos KDC 服务提供的互操作特性。
使用以下命令配置 SPN(针对 AES128 加密强度)并生成 keytab 文件:
C:\Users\bt>ktpass -out negotiatetestserver_keytab -princ negotiatetestserver@SECURITYQA.COM -mapUser negotiatetestserver -kvno 0 -crypto AES128-SHA1 -pass <password> -p type KRB5_NT_PRINCIPAL
将生成的 keytab 文件 (negotiatetestserver_keytab) 保存在安全位置,并将其导出到 Oracle WebLogic Server 的域目录。(在本例中,将此文件传输到 MachineB。)稍后说明的 JAAS(Java 身份验证和授权服务)配置文件将引用此文件。
对该服务器的配置有以下重要要求:
JAAS 允许动态配置登录模块。我们需要指定一个 JAAS 配置文件以指定使用的登录模块。
使用以下内容在 Oracle WebLogic Server 域目录中创建一个名为 krb5Login.conf 的文件:
对于使用 Oracle JDK 的 Oracle WebLogic Server:
com.sun.security.jgss.initiate {
com.sun.security.auth.module.Krb5LoginModule required principal="negotiatetestserver@SECURITYQA.COM"
useKeyTab=true keyTab=negotiatetestserver_keytab
storeKey=true debug=true;
};
com.sun.security.jgss.krb5.accept {
com.sun.security.auth.module.Krb5LoginModule required principal="negotiatetestserver@SECURITYQA.COM"
useKeyTab=true keyTab=negotiatetestserver_keytab
storeKey=true debug=true;
};
对于使用 IBM JDK 的 Oracle WebLogic Server:
com.ibm.security.jgss.initiate {
com.ibm.security.auth.module.Krb5LoginModule required principal="negotiatetestserver@SECURITYQA.COM"
useKeyTab=true keyTab=negotiatetestserver_keytab storeKey=true debug=true;
};
com.ibm.security.jgss.accept {
com.ibm.security.auth.module.Krb5LoginModule required principal="negotiatetestserver@SECURITYQA.COM"
useKeyTab=true keyTab=negotiatetestserver_keytab storeKey=true debug=true;
};
这里假设您已将第 2 步中生成的 keytab 文件“negotiatetestserver_keytab”传送到 Oracle WebLogic Server 上的域目录。如果 Oracle WebLogic Server 使用 Oracle JDK,请在 Oracle WebLogic Server java 命令行指定以下选项:
-Dsun.security.krb5.debug=true -Djava.security.krb5.realm=SECURITYQA.COM -Djava.security.krb5.kdc=MACHINEC -Djava.security.auth.login.config= krb5Login.conf -Djavax.security.auth.useSubjectCredsOnly=false
对于使用 IBM JDK 的 Oracle WebLogic Server,在 Oracle WebLogic Server java 命令行指定以下选项:
-Dcom.ibm.security.jgss.debug=all -Djava.security.krb5.realm=SECURITYQA.COM -Djava.security.krb5.kdc=MACHINEC -Djava.security.auth.login.config= krb5Login.conf -Djavax.security.auth.useSubjectCredsOnly=false
WebLogic Server 包括一个安全提供程序(Negotiate Identity Assertion 提供程序)以支持 Microsoft 客户端一次性登录 (SSO)。此身份断言提供程序对 SPNEGO(简单和受保护协商)令牌进行解码以获取 Kerberos 令牌,验证 Kerberos 令牌并将 Kerberos 令牌映射到 WebLogic 用户。您需要在 WebLogic 安全领域内配置 Negotiate Identity Assertion 提供程序以便支持 Microsoft 客户端进行 SSO。请参见配置 Negotiate Identity Assertion 提供程序。
(此步骤仅适用于打算使用 AES256-SHA1 加密强度的情况。对于所有其他加密强度,请跳过此步骤)。您需要下载和安装此文件包,它提供的“无限强度”策略文件不含对加密强度的任何限制。
为了进行身份验证,必须在参与客户端一次性登录的 Web 应用程序中,对被访问的资源(JSP 或 Servlet)实施保护。
以下是本例中所用的 servlet 代码 (SimpleTestServlet.java):
package wlstest.functional.security.negotiate.servlet;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Enumeration;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpUtils;
public class SimpleTestServlet extends HttpServlet
{
public void service(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException
{
res.setContentType("text/html");
PrintWriter out = res.getWriter();
out.println("<html>");
out.println("<head><title>Simple Test Servlet</title></head>");
out.println("<body>");
out.println("<h3>Requested URL:</h3>");
out.println("<pre>");
out.println(HttpUtils.getRequestURL(req).toString());
out.println("</pre>");
Enumeration theEnum = getServletConfig().getInitParameterNames();
if (theEnum != null)
{
boolean first = true;
while (theEnum.hasMoreElements())
{
if (first)
{
out.println("<h3>Init Parameters</h3>");
out.println("<pre>");
first = false;
}
String param = (String) theEnum.nextElement();
out.println(" "+param+": "+getInitParameter(param));
}
out.println("</pre>");
}
out.println("<h3>Request information:</h3>");
out.println("<pre>");
print(out, "Request method", req.getMethod());
print(out, "Request URI", req.getRequestURI());
print(out, "Request protocol", req.getProtocol());
print(out, "Servlet path", req.getServletPath());
print(out, "Path info", req.getPathInfo());
print(out, "Path translated", req.getPathTranslated());
print(out, "Query string", req.getQueryString());
print(out, "Content length", req.getContentLength());
print(out, "Content type", req.getContentType());
print(out, "Server name", req.getServerName());
print(out, "Server port", req.getServerPort());
print(out, "Remote user", req.getRemoteUser());
print(out, "Remote address", req.getRemoteAddr());
print(out, "Remote host", req.getRemoteHost());
print(out, "Scheme", req.getScheme());
print(out, "Authorization scheme", req.getAuthType());
print(out, "Request scheme", req.getScheme());
out.println("</pre>");
Enumeration e = req.getHeaderNames();
if (e.hasMoreElements())
{
out.println("<h3>Request headers:</h3>");
out.println("<pre>");
while (e.hasMoreElements())
{
String name = (String)e.nextElement();
out.println(" " + name + ": " + req.getHeader(name));
}
out.println("</pre>");
}
e = req.getParameterNames();
if (e.hasMoreElements())
{
out.println("<h3>Servlet parameters (Single Value style):</h3>");
out.println("<pre>");
while (e.hasMoreElements())
{
String name = (String)e.nextElement();
out.println(" " + name + " = " + req.getParameter(name));
}
out.println("</pre>");
}
e = req.getParameterNames();
if (e.hasMoreElements())
{
out.println("<h3>Servlet parameters (Multiple Value style):</h3>");
out.println("<pre>");
while (e.hasMoreElements())
{
String name = (String)e.nextElement();
String vals[] = (String []) req.getParameterValues(name);
if (vals != null)
{
out.print("<b> " + name + " = </b>");
out.println(vals[0]);
for (int i = 1; i<vals.length; i++)
out.println(" " + vals[i]);
}
out.println("<p>");
}
out.println("</pre>");
}
out.println("<h3>Request Attributes:</h3>");
e = req.getAttributeNames();
if (e.hasMoreElements())
{
out.println("<pre>");
while (e.hasMoreElements())
{
String name = (String)e.nextElement();
Object o = req.getAttribute(name);
if (o == null) continue;
out.println(" " + name + ": type=" + o.getClass().getName() + " str='" + o.toString() + "'");
}
out.println("</pre>");
}
out.println("</body></html>");
}
private void print (PrintWriter out, String name, String value)
{
out.print(" " + name + ": ");
out.println(value == null ? "<none>" : value);
}
private void print (PrintWriter out, String name, int value)
{
out.print(" " + name + ": ");
if (value == -1)
out.println("<none>");
else
out.println(value);
}
}
web.xml 文件如下所示:
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 1.2//EN" "http://java.sun.com/j2ee/dtds/web-app_2_2.dtd">
<web-app>
<display-name>Simple Test Servlet (Basic Auth)</display-name>
<security-constraint>
<web-resource-collection>
<web-resource-name>BasicAuthSimpleTestServlet</web-resource-name>
<url-pattern>/*</url-pattern>
<url-pattern>/</url-pattern>
<http-method>POST</http-method>
<http-method>GET</http-method>
</web-resource-collection>
<auth-constraint>
<role-name>negotiateAdminRole</role-name>
</auth-constraint>
<user-data-constraint>
<description>no description</description>
<transport-guarantee>NONE</transport-guarantee>
</user-data-constraint>
</security-constraint>
<security-role>
<role-name>negotiateAdminRole</role-name>
</security-role>
<login-config>
<auth-method>BASIC</auth-method>
<realm-name>default</realm-name>
</login-config>
<servlet>
<servlet-name>BasicAuthSimpleTestServlet</servlet-name>
<servlet-class>wlstest.functional.security.negotiate.servlet.SimpleTestServlet</servlet-class>
</servlet>
<welcome-file-list>
<welcome-file>/BasicAuthSimpleTestServlet</welcome-file>
</welcome-file-list>
<servlet-mapping>
<servlet-name>BasicAuthSimpleTestServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
角色 negotiateAdminRole 在 weblogic.xml 中进行定义,如下所示:
<!DOCTYPE weblogic-web-app PUBLIC "-//BEA Systems, Inc.//DTD Web Application 8.1//EN" "http://www.bea.com/servers/wls810/dtd/weblogic810-web-jar.dtd">
<weblogic-web-app>
<security-role-assignment>
<role-name>negotiateAdminRole</role-name>
<principal-name>negotiateAdmin</principal-name>
<principal-name>Administrators</principal-name>
</security-role-assignment>
</weblogic-web-app>
可以从这里下载本文所使用的 servlet 和相关文件。
为了进行一次性登录,需要一个经过身份验证的 Microsoft 客户端,它属于由您的领域所控制的域,并且请求访问 Oracle WebLogic Server 服务。
要配置 Internet Explorer 浏览器以使用 Windows 身份验证,请在 Internet Explorer 中按照以下过程进行配置。
配置本地内联网域 1. 在 Internet Explorer 中,选择“工具”>“Internet 选项”。
2. 选择“安全”选项卡。
3. 选择“本地 Intranet”并单击“站点”。
4. 在“本地 Intranet”弹出窗口中,确保选中“包括所有不使用代理服务器的站点”和“包括没有列在其他区域的所有本地(Intranet)站点”选项。

图 3:Internet Explorer“本地 Intranet”对话框
5. 单击“高级”。
6. 在“本地 Intranet”(高级)对话框中,添加参与 SSO 配置的 Oracle WebLogic Server 实例将使用的所有相对域名(例如 myhost.example.com)并单击“确定”。

图 4:Internet Explorer 的高级“本地 Intranet”对话框
1. 选择“工具”>“Internet 选项”。
2. 选择“安全”选项卡。
3. 选择“本地 Intranet”并单击“自定义级别...”。.
4. 在“安全设置”对话框中,滚动到“用户验证”部分。
5. 选择“只在 Intranet 区域自动登录”。此选项可使用户不必重新输入登录凭证,这是该解决方案的一个重要部分。
6. 单击“确定”。

图 5:配置内联网身份验证
如果启用了代理服务器:
1. 选择“工具”>“Internet 选项”。
2. 选择“连接”选项卡并单击“局域网设置”。
3. 验证代理服务器地址和端口号是否正确。
4. 单击“高级”。
5. 在“代理服务器设置”对话框中,确保在“例外情况”域中输入所有需要的域名。
6. 单击“确定”关闭“代理服务器设置”对话框。
要配置 Firefox 浏览器使用 Windows 集成身份验证,请完成以下步骤:
1. 启动 Firefox。
2. 在位置栏中输入 about:config。
3. 输入筛选字符串 network.negotiate。
4. 按照下图所示设置首选项:

图 6:在 Firefox 中实现 Windows 集成身份验证所需的首选项
对于 Chrome 浏览器,无需特别配置。
验证配置

图 7:使用 klist 查看和清除票证

图 8:在 SPNEGO 身份验证后显示 HTTP 信息的 Servlet

图 9:在 SPNEGO 失败后浏览器提示输入用户名/口令
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:7.0.1) Gecko/20100101 Firefox/7.0.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip, deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Authorization: Negotiate YIIGzQYGKwYBBQUCoIIGwTCCBr2gMDAuBgkqhkiC9xIBAgIGCSqGSIb3EgECAgYKKwYBBAGCNwICHgYKKwYBBAGCNwICCqKCB
ocEggaDYIIGfwYJKoZIhvcSAQICAQBuggZuMIIGaqADAgEFoQMCAQ6iBwMFACAAAACjggUCYYIE/jCCBPqgAwIBBaEQGw5TRUNVUklUWVFBLkNPTaIrMCmgAwIBA
qEiMCAbBEhUVFAbGGFkYzIxNzA3MTkudXMub3JhY2xlLmNvbaOCBLIwggSuoAMCARGhAwIBJKKCBKAEggSc8v4RphGvP7CinPf4mhiBzyfZWQG …
对于 Oracle JDK:
>>>Pre-Authentication Data:
PA-DATA type = 19
PA-ETYPE-INFO2 etype = 17
>>>Pre-Authentication Data:
PA-DATA type = 2
PA-ENC-TIMESTAMP
>>>Pre-Authentication Data:
PA-DATA type = 16
>>>Pre-Authentication Data:
PA-DATA type = 15
AcquireTGT: PREAUTH FAILED/REQUIRED, re-send AS-REQ
Updated salt from pre-auth = SECURITYQA.COMnegotiatetestserver
>>>KrbAsReq salt is SECURITYQA.COMnegotiatetestserver
Pre-Authenticaton: find key for etype = 17
AS-REQ: Add PA_ENC_TIMESTAMP now
>>> EType: sun.security.krb5.internal.crypto.Aes128CtsHmacSha1EType
>>> KrbAsReq calling createMessage
>>> KrbAsReq in createMessage
>>> KrbKdcReq send: kdc=rno05089 UDP:88, timeout=30000, number of retries =3, #bytes=241
>>> KDCCommunication: kdc=rno05089 UDP:88, timeout=30000,Attempt =1, #bytes=241
>>> KrbKdcReq send: #bytes read=100
>>> KrbKdcReq send: #bytes read=100
>>> KdcAccessibility: remove rno05089
>>> KDCRep: init() encoding tag is 126 req type is 11
对于 IBM JDK:
JGSS_DBG_PROV] getMechs: Mechanism(s) supported by provider IBMJGSSProvider
[JGSS_DBG_PROV] 1.3.6.1.5.5.2
[JGSS_DBG_PROV] getMechs: Mechanism(s) supported by provider IBMJGSSProvider
[JGSS_DBG_PROV] 1.2.840.113554.1.2.2
[JGSS_DBG_PROV] getMechs: Mechanism(s) supported by provider IBMSPNEGO
[JGSS_DBG_PROV] 1.3.6.1.5.5.2
[JGSS_DBG_PROV] getMechs: 2 unique mechanism(s) found
[JGSS_DBG_PROV] [0]: 1.3.6.1.5.5.2
[JGSS_DBG_PROV] [1]: 1.2.840.113554.1.2.2
[JGSS_DBG_CTX] Default list of negotiable mechs:
1.2.840.113554.1.2.2
[JGSS_DBG_CTX] AuthenticatorCache, scope of bucket122
[JGSS_DBG_CTX] ticket enc type = rc4-hmac
[JGSS_DBG_CTX] Successfully decrypted ticket
[JGSS_DBG_CTX] Put authz info in cache
[JGSS_DBG_CTX] Session key type = rc4-hmac
[JGSS_DBG_CTX] Successfully decrypted authenticator
[JGSS_DBG_CTX] Remote subkey type = rc4-hmac
[JGSS_DBG_CTX] No delegated creds from peer
[JGSS_DBG_CTX] Received channel binding checksum
以下是 SPNEGO 配置期间可能遇到的典型异常以及解决方案:
SSO 跨平台身份验证是通过模拟使用 Kerberos 协议的原生 Windows 到 Windows 身份验证服务的协商行为来实现的。为使跨平台身份验证起作用,可以使用 Oracle WebLogic Server 解析 SPNEGO 令牌以提取 Kerberos 令牌,然后使用令牌进行身份验证,从而为最终用户提供透明的身份验证。
Abhijit Patil 是 Oracle Weblogic Server 组技术人员中的主要成员。他在各种 Weblogic Server 技术方面(包括安全性、Web 服务、服务器集群等)拥有 10 多年的工作经验。