从Java验证SPNEGO票证是一个有点复杂的过程。这是一个简短的概述,但请记住,该过程可能有很多陷阱。你真的需要了解Active Directory,Kerberos,SPNEGO和JAAS如何运作以成功诊断问题。
在开始之前,请确保您知道 Windows 域的 kerberos 领域名称。出于这个答案的目的,我将假设它是MYDOMAIN。您可以通过从 cmd 窗口运行来获取领域名称。请注意,kerberos 区分大小写,并且领域几乎总是全部大写。echo %userdnsdomain%
步骤 1 - 获取 Kerberos 密钥表
为了使 kerberos 客户端能够访问服务,它会请求表示该服务的服务主体名称 [SPN] 的票证。SPN 通常派生自计算机名称和被访问的服务类型(例如 )。为了验证特定 SPN 的 kerberos 票证,您必须具有一个密钥表文件,其中包含 Kerberos 域控制器 [KDC] 票证授予票证 [TGT] 服务和服务提供商(您)都知道的共享机密。HTTP/www.my-domain.com
就 Active Directory 而言,KDC 是域控制器,共享机密只是拥有 SPN 的帐户的纯文本密码。SPN 可能由 AD 中的计算机或用户对象拥有。
如果要定义服务,则在 AD 中设置 SPN 的最简单方法是设置基于用户的 SPN,如下所示:
- 在AD中创建一个密码不会过期的未priviledge服务帐户,例如使用密码SVC_HTTP_MYSERVER
ReallyLongRandomPass
-
使用 Windows 实用工具将服务 SPN 绑定到帐户。最佳做法是为主机的短名称和 FQDN 定义多个 SPN:setspn
setspn -U -S HTTP/myserver@MYDOMAIN SVC_HTTP_MYSERVER
setspn -U -S HTTP/myserver.my-domain.com@MYDOMAIN SVC_HTTP_MYSERVER
-
使用 Java 的实用程序为帐户生成密钥表。ktab
ktab -k FILE:http_myserver.ktab -a HTTP/myserver@MYDOMAIN ReallyLongRandomPass
ktab -k FILE:http_myserver.ktab -a HTTP/myserver.my-domain.com@MYDOMAIN ReallyLongRandomPass
如果您尝试对绑定到计算机帐户或不受您控制的用户帐户的预先存在的 SPN 进行身份验证,则上述操作将不起作用。您需要从 ActiveDirectory 本身中提取密钥表。Wireshark Kerberos页面对此有一些很好的指示。
步骤 2 - 设置 krb5.conf
在创建描述您的域的 krb5.conf 中。请确保在此处定义的领域与为 SPN 设置的领域相匹配。如果不将文件放在 JVM 目录中,可以通过在命令行上进行设置来指向它。%JAVA_HOME%/jre/lib/security
-Djava.security.krb5.conf=C:\path\to\krb5.conf
例:
[libdefaults]
default_realm = MYDOMAIN
[realms]
MYDOMAIN = {
kdc = dc1.my-domain.com
default_domain = my-domain.com
}
[domain_realm]
.my-domain.com = MYDOMAIN
my-domain.com = MYDOMAIN
步骤 3 - 设置 JAAS login.conf
您的 JAAS 应定义一个登录配置,将 Krb5LoginModule 设置为接受器。下面是一个示例,它假定我们上面创建的 keytab 位于 中。通过在命令行上进行设置指向 JASS 配置文件。login.conf
C:\http_myserver.ktab
-Djava.security.auth.login.config=C:\path\to\login.conf
http_myserver_mydomain {
com.sun.security.auth.module.Krb5LoginModule required
principal="HTTP/myserver.my-domain.com@MYDOMAIN"
doNotPrompt="true"
useKeyTab="true"
keyTab="C:/http_myserver.ktab"
storeKey="true"
isInitiator="false";
};
或者,您可以在运行时生成 JAAS 配置,如下所示:
public static Configuration getJaasKrb5TicketCfg(
final String principal, final String realm, final File keytab) {
return new Configuration() {
@Override
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
Map<String, String> options = new HashMap<String, String>();
options.put("principal", principal);
options.put("keyTab", keytab.getAbsolutePath());
options.put("doNotPrompt", "true");
options.put("useKeyTab", "true");
options.put("storeKey", "true");
options.put("isInitiator", "false");
return new AppConfigurationEntry[] {
new AppConfigurationEntry(
"com.sun.security.auth.module.Krb5LoginModule",
LoginModuleControlFlag.REQUIRED, options)
};
}
};
}
您将为此配置创建一个 LoginContext,如下所示:
LoginContext ctx = new LoginContext("doesn't matter", subject, null,
getJaasKrbValidationCfg("HTTP/myserver.my-domain.com@MYDOMAIN", "MYDOMAIN",
new File("C:/path/to/my.ktab")));
第4步 - 接受机票
这有点即兴,但一般的想法是定义一个PriviledgedAction,该操作使用票证执行SPNEGO协议。请注意,此示例不检查 SPNEGO 协议是否完整。例如,如果客户端请求服务器身份验证,则需要返回在 HTTP 响应的身份验证标头中生成的令牌。acceptSecContext()
public class Krb5TicketValidateAction implements PrivilegedExceptionAction<String> {
public Krb5TicketValidateAction(byte[] ticket, String spn) {
this.ticket = ticket;
this.spn = spn;
}
@Override
public String run() throws Exception {
final Oid spnegoOid = new Oid("1.3.6.1.5.5.2");
GSSManager gssmgr = GSSManager.getInstance();
// tell the GSSManager the Kerberos name of the service
GSSName serviceName = gssmgr.createName(this.spn, GSSName.NT_USER_NAME);
// get the service's credentials. note that this run() method was called by Subject.doAs(),
// so the service's credentials (Service Principal Name and password) are already
// available in the Subject
GSSCredential serviceCredentials = gssmgr.createCredential(serviceName,
GSSCredential.INDEFINITE_LIFETIME, spnegoOid, GSSCredential.ACCEPT_ONLY);
// create a security context for decrypting the service ticket
GSSContext gssContext = gssmgr.createContext(serviceCredentials);
// decrypt the service ticket
System.out.println("Entering accpetSecContext...");
gssContext.acceptSecContext(this.ticket, 0, this.ticket.length);
// get the client name from the decrypted service ticket
// note that Active Directory created the service ticket, so we can trust it
String clientName = gssContext.getSrcName().toString();
// clean up the context
gssContext.dispose();
// return the authenticated client name
return clientName;
}
private final byte[] ticket;
private final String spn;
}
然后,要对票证进行身份验证,可以执行以下操作。假定 包含来自身份验证标头的已以 64 为基数解码的票证。如果格式为 .,则应从 HTTP 请求中的标头派生。例如,如果标头是 则应该是 .ticket
spn
Host
HTTP/<HOST>@<REALM>
Host
myserver.my-domain.com
spn
HTTP/myserver.my-domain.com@MYDOMAIN
public boolean isTicketValid(String spn, byte[] ticket) {
LoginContext ctx = null;
try {
// this is the name from login.conf. This could also be a parameter
String ctxName = "http_myserver_mydomain";
// define the principal who will validate the ticket
Principal principal = new KerberosPrincipal(spn, KerberosPrincipal.KRB_NT_SRV_INST);
Set<Principal> principals = new HashSet<Principal>();
principals.add(principal);
// define the subject to execute our secure action as
Subject subject = new Subject(false, principals, new HashSet<Object>(),
new HashSet<Object>());
// login the subject
ctx = new LoginContext("http_myserver_mydomain", subject);
ctx.login();
// create a validator for the ticket and execute it
Krb5TicketValidateAction validateAction = new Krb5TicketValidateAction(ticket, spn);
String username = Subject.doAs(subject, validateAction);
System.out.println("Validated service ticket for user " + username
+ " to access service " + spn );
return true;
} catch(PriviledgedActionException e ) {
System.out.println("Invalid ticket for " + spn + ": " + e);
} catch(LoginException e) {
System.out.println("Error creating validation LoginContext for "
+ spn + ": " + e);
} finally {
try {
if(ctx!=null) { ctx.logout(); }
} catch(LoginException e) { /* noop */ }
}
return false;
}