【问题标题】:Using GSSManager to validate a Kerberos ticket使用 GSSManager 验证 Kerberos 票证
【发布时间】:2014-08-13 14:44:28
【问题描述】:

我有以下代码:

public static void main(String args[]){
    try {
        //String ticket = "Negotiate YIGCBg...==";
        //byte[] kerberosTicket = ticket.getBytes();
        byte[] kerberosTicket = Base64.decode("YIGCBg...==");
        GSSContext context = GSSManager.getInstance().createContext((GSSCredential) null);
        context.acceptSecContext(kerberosTicket, 0, kerberosTicket.length);
        String user = context.getSrcName().toString();
        context.dispose();
    } catch (GSSException e) {
        e.printStackTrace();
    } catch (Base64DecodingException e) {
        e.printStackTrace();
    }
}

当然失败了。这是一个例外:

GSSException: Defective token detected (Mechanism level: GSSHeader did not find the right tag)

我不知道该怎么做才能解决这个问题。老实说,我不太了解 Kerberos。

我通过发送带有适当标题WWW-Authenticate 的 401 以“协商”作为值来获得这张票。浏览器立即再次发出相同的请求,并带有包含此票证的 authorization 标头。

我希望我可以验证票证并确定用户是谁。

我需要密钥表文件吗?如果是这样,我将在什么凭据下运行它?我正在尝试将 Kerberos 票证用于网站的身份验证。凭据是来自 IIS 的凭据吗?

我错过了什么?


更新 1 从 Michael-O 的回复中,我进行了更多的谷歌搜索并找到了this article,这导致我找到了this article

table 3,我找到了1.3.6.1.5.5.2 SPNEGO

我现在已按照第一篇文章中的示例将其添加到我的凭据中。这是我的代码:

public static void main(String args[]){
    try {            
        Oid mechOid = new Oid("1.3.6.1.5.5.2");

        GSSManager manager = GSSManager.getInstance();

        GSSCredential myCred = manager.createCredential(null,
                GSSCredential.DEFAULT_LIFETIME,
                mechOid,
                GSSCredential.ACCEPT_ONLY);

        GSSContext context = manager.createContext(myCred);

        byte[] ticket = Base64.decode("YIGCBg...==");
        context.acceptSecContext(ticket, 0, ticket.length);
        String user = context.getSrcName().toString();
        context.dispose();
    } catch (GSSException e) {
        e.printStackTrace();
    } catch (Base64DecodingException e) {
        e.printStackTrace();
    }
}

但现在代码在 createCredential 上失败并出现此错误:

GSSException: No valid credentials provided (Mechanism level: Failed to find any Kerberos credentails)

这是整张票:YIGCBgYrBgEFBQKgeDB2oDAwLgYKKwYBBAGCNwICCgYJKoZIgvcSAQICBgkqhkiG9xIBAgIGCisGAQQBgjcCAh6iQgRATlRMTVNTUAABAAAAl7II4g4ADgAyAAAACgAKACgAAAAGAbEdAAAAD0xBUFRPUC0yNDVMSUZFQUNDT1VOVExMQw==

【问题讨论】:

    标签: java kerberos gssapi


    【解决方案1】:

    从 Java 验证 SPNEGO 票证是一个有点复杂的过程。这是一个简短的概述,但请记住,该过程可能有很多陷阱。您确实需要了解 Active Directory、Kerberos、SPNEGO 和 JAAS 都是如何运行才能成功诊断问题的。

    在开始之前,请确保您知道您的 Windows 域的 kerberos 领域名称。出于这个答案的目的,我假设它是 MYDOMAIN。您可以通过在 cmd 窗口中运行 echo %userdnsdomain% 来获取领域名称。请注意,kerberos 区分大小写,并且领域几乎总是全部大写。

    第 1 步 - 获取 Kerberos 密钥表

    为了让 kerberos 客户端访问服务,它会请求代表该服务的服务主体名称 [SPN] 的票证。 SPN 通常源自机器名称和正在访问的服务类型(例如HTTP/www.my-domain.com)。为了验证特定 SPN 的 kerberos 票证,您必须拥有一个密钥表文件,其中包含 Kerberos 域控制器 [KDC] 票证授予票证 [TGT] 服务和服务提供商(您)都知道的共享机密。

    就 Active Directory 而言,KDC 是域控制器,而共享机密只是 拥有 SPN 的帐户的纯文本密码。 SPN 可能由 AD 中的计算机或用户对象拥有。

    如果要定义服务,在 AD 中设置 SPN 的最简单方法是设置基于用户的 SPN,如下所示:

    1. 在 AD 中创建一个无特权的服务帐户,其密码不会过期,例如SVC_HTTP_MYSERVER,密码为ReallyLongRandomPass
    2. 使用 windows setspn 实用程序将服务 SPN 绑定到帐户。最佳做法是为主机的短名称和 FQDN 定义多个 SPN:

      setspn -U -S HTTP/myserver@MYDOMAIN SVC_HTTP_MYSERVER
      setspn -U -S HTTP/myserver.my-domain.com@MYDOMAIN SVC_HTTP_MYSERVER
      
    3. 使用 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 Page 对此有一些很好的指导。

    第 2 步 - 设置您的 krb5.conf

    %JAVA_HOME%/jre/lib/security 创建一个描述您的域的 krb5.conf。确保您在此处定义的领域与您为 SPN 设置的领域相匹配。如果不把文件放到JVM目录下,可以在命令行设置-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 login.conf 应该定义一个登录配置,将 Krb5LoginModule 设置为接受器。这是一个假设我们在上面创建的 keytab 位于C:\http_myserver.ktab 中的示例。通过在命令行中设置-Djava.security.auth.login.config=C:\path\to\login.conf 指向JASS 配置文件。

    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 步 - 接受票证

    这有点不合时宜,但总体思路是定义一个使用票证执行 SPNEGO 协议的 PrivilegedAction。请注意,此示例不检查 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;
    }
    

    然后,要验证票证,您将执行以下操作。假设 ticket 包含来自身份验证标头的已经经过 base-64 解码的票证。如果格式为HTTP/&lt;HOST&gt;@&lt;REALM&gt;,则spn 应派生自HTTP 请求中的Host 标头。例如。如果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;
    }
    

    【讨论】:

    • 另外,为什么 kerberos mech id "1.2.840.113554.1.2.2"?我以为这是spnego
    • @DominicA。感谢您提供有用的信息。我对生成 Krb 配置的代码 sn-p 有疑问,您设置了“领域”选项,但我在 Oracle Krb5LoginModule 文档中看不到此选项。这是一个错误还是“未记录的内部结构”?
    • @FabianoTarlao,你是对的。领域属性没有任何效果。我相信您需要通过java.security.krb5.realm 系统属性指定领域,或者在本例中,领域可以从主体名称中的@ 语法派生。
    • 我编写了受您的 sn-p 启发的代码,并且在没有领域选项的情况下“简单地工作”。目前,我提供了一个具有全名 (user@DOMAIN) 的主体。我想您对系统属性或主体全名提取的域是正确的。问候
    • /MTDOMAIN/MYDOMAIN/
    【解决方案2】:

    这不是 Kerberos 票证,而是 SPNEGO 票证。你的上下文有错误的机制。

    编辑:尽管您现在拥有正确的机甲,但您的客户端正在向您发送 GSS-API 无法处理的 NTLM 令牌。获取 Base 64 令牌,解码为原始字节并显示 ASCII 字符。如果它以NTLMSSP 开头,那么它肯定不会工作,并且您已经破坏了 Kerberos 设置。

    编辑 2:这是你的票:

    60 81 82 06 06 2B 06 01 05 05 02 A0 78 30 76 A0 30 30 2E 06  `..+..... x0v 00..
    0A 2B 06 01 04 01 82 37 02 02 0A 06 09 2A 86 48 82 F7 12 01  .+....7.....*H÷..
    02 02 06 09 2A 86 48 86 F7 12 01 02 02 06 0A 2B 06 01 04 01  ....*H÷......+....
    82 37 02 02 1E A2 42 04 40 4E 54 4C 4D 53 53 50 00 01 00 00  7...¢B.@NTLMSSP....
    00 97 B2 08 E2 0E 00 0E 00 32 00 00 00 0A 00 0A 00 28 00 00  .².â....2.......(..
    00 06 01 B1 1D 00 00 00 0F 4C 41 50 54 4F 50 2D 32 34 35 4C  ...±.....LAPTOP-245L
    49 46 45 41 43 43 4F 55 4E 54 4C 4C 43                       IFEACCOUNTLLC       
    

    这是 SPNEGO 令牌内的包装 NTLM 令牌。这仅仅意味着 Kerberos 由于某些原因失败了,例如,

    • SPN 未注册
    • 时钟偏差
    • Kerberos 不允许
    • DNS 记录不正确

    最好的选择是在客户端使用 Wireshark 来查找根本原因。

    请注意,Java 确实支持 NTLM 作为 SPNEGO 子机制。 NTLM 仅受 SSPI 和 Heimdal 支持。

    【讨论】:

    • @JoshC,你能贴出整张票吗?
    • 因此,我通过查看在 IIS 中使用 Windows 身份验证的工作站点的 http 流量获得了这张票。我没有指定我的 WWW-Authenticate 标头值。这是Negotiate。那会有什么不同吗?同样,我试图以一个工作站点为例,并对其进行逆向工程以使用开源技术。
    • @JoshC.,你正在尝试什么是不可能的。就 JGSS 的能力而言,没有接受者的凭据,您无法解密票证,接受者是机器而不是您。
    【解决方案3】:

    如果服务器没有注册 KDC 的 keytab 和关联密钥,您将永远无法使用 kerberos 验证票证。

    让 SPNEGO 工作充其量是棘手的,如果至少粗略了解 kerberos 的工作原理,几乎是不可能的。尝试阅读此对话框,看看是否可以更好地理解。

    http://web.mit.edu/kerberos/dialogue.html

    SPNEGO 需要 HTTP/server.example.com 形式的 SPN,并且您需要在启动服务器时告诉 GSS 库该密钥表在哪里。

    【讨论】:

    • 我在本地主机上运行它。我怎么知道它是否有 keytab 文件或它在哪里?
    • 如果你不知道它在哪里,你可能没有它。默认情况下不安装 SPNEGO 的 Keytab。您可以使用 ktutil 查看 /etc/krb5.keytab,但这只是猜测。
    • 我有一个支持 Windows 身份验证的 .Net 站点。当我拉取源并在本地调试时,它可以正常工作。现在,我正在尝试在.Net 之外做同样的事情。是否有应用程序可以共享的“默认”密钥表文件?
    • 对不起,没有线索。我只从 Unix/Apache 方面了解这些东西。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-10-20
    • 1970-01-01
    • 2011-05-29
    • 2011-12-23
    • 2016-02-17
    • 1970-01-01
    • 2011-01-14
    相关资源
    最近更新 更多