java - 如何验证kerberos token ?

标签 java active-directory kerberos spnego gssapi

所以又是我遇到了一些 AD 和 Kerberos 问题。

好吧,我从 WWW-Authenticate header 中获得了一个 kerberos token 。现在我想根据 AD 验证此 token ,但我不知道如何。

我从 GSSAPI 中找到了一些东西,但没有看到将 byte[] 用作 Kerberos token 或任何其他方式的函数或方法。

我正在运行一个 Java EE 网络应用程序。

我可以用这个 token 做什么来从 AD 获取用户,尤其是“这个 token 和用户是合法的”?

编辑:

正如评论中所说,我真的很接近能够执行 SSO。所以我会用我所拥有的更新你们。

我有一个名为 ping01 的服务器和我的本地机器。作为用户 mgmt 在 Tomcat 上运行我有我的应用程序。所以我做了这一切:

在用户管理处创建了 SPN HTTP/ping01.cool.domain@COOL.DOMAIN。

krb5.conf:

[libdefaults]
default_tkt_enctypes = aes256-cts rc4-hmac des3-cbc-sha1 des-cbc-md5 des-cbc-crc
default_tgs_enctypes = aes256-cts rc4-hmac des3-cbc-sha1 des-cbc-md5 des-cbc-crc
permitted_enctypes   = aes256-cts rc4-hmac des3-cbc-sha1 des-cbc-md5 des-cbc-crc
default_realm = COOL.DOMAIN
kdc_timesync = 1
ccache_type = 5
forwardable = true
proxiable = true

[realms]
COOL.DOMAIN = {
kdc = kdc.cool.domain
admin_server = COOL.DOMAIN
default_domain = COOL.DOMAIN
}

[domain_realm]
cool.domain = COOL.DOMAIN
ping01 = COOL.DOMAIN



[login]
krb4_convert = true
krb4_get_tickets = false

我还有这个代码:

 /**
   * Gets the jaas krb 5 ticket cfg.
   *
   * @param principal the principal
   * @param realm the realm
   * @param keyTab the key tab
   * @return the jaas krb 5 ticket cfg
   */
  private 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<>();
        options.put( "principal", principal );
        options.put( "realm", realm );
        options.put( "doNotPrompt", "true" );
        options.put( "useKeyTab", "true" );
        options.put( "keyTab", keyTab.getAbsolutePath() );
        options.put( "storeKey", "true" );
        options.put( "isInitiator", "false" );

        return new AppConfigurationEntry[] {
            new AppConfigurationEntry( "com.sun.security.auth.module.Krb5LoginModule", LoginModuleControlFlag.REQUIRED, options ) };
      }
    };
  }

  /** {@inheritDoc} */
  @Override
  public boolean isTicketValid( String spn, byte[] ticket )
  {
    LoginContext ctx = null;
    try
    {
      /** define the principal who will validate the ticket */
      Principal principal = new KerberosPrincipal( spn, KerberosPrincipal.KRB_NT_SRV_INST );
      Set<Principal> principals = new HashSet<>();
      principals.add( principal );

      /** define the subject to execute our secure action as */
      Subject subject = new Subject( false, principals, new HashSet<>(), new HashSet<>() );

      /** login the subject */
      /**
       * TODO: Find the correct way to use the commented out version!
       */
      // ctx = new LoginContext( "http_ping01_domain", subject );
      ctx = new LoginContext( "doesn't matter", subject, null,
          getJaasKrb5TicketCfg( "HTTP/ping01.cool.domain@COOL.DOMAIN", "COOL.DOMAIN",
              new File( "http_ping01_test.ktab" ) ) );
      ctx.login();

      /** create a validator for the ticket and execute it */
      SingleSignOnImpl validateAction = new SingleSignOnImpl( ticket, spn, log );
      String username = Subject.doAs( subject, validateAction );
      log.info( "Validated service ticket for user " + username + " to access service " + spn );
      return true;
    }
    catch ( PrivilegedActionException e )
    {
      /**
       * Error reasons for this Exception: - Incorrect Kerberos Mechanism - Incorrect Token received
       * - Incorrect keytab
       */
      log.error( "Invalid ticket for " + spn + ": " + e );
    }
    catch ( LoginException e )
    {
      /**
       * Error reasons for this Exception: - False krb5.conf (can't reach KDC) - incorrect SPN -
       * False login.conf
       */
      log.error( "Error creating validation LoginContext for " + spn + ": " + e );
    }
    finally
    {
      try
      {
        if ( ctx != null )
        {
          ctx.logout();
        }
      }
      catch ( LoginException e )
      {
        log.error( "" + e );
      }
    }

    return false;
  }

除此之外,我在一个单独的类中拥有 privilegedAction:

public class SingleSignOnImpl implements PrivilegedExceptionAction<String>
{

  /** The ticket from the client. */
  private final byte[] ticket;

  /** The Service principal name (SPN). */
  private final String spn;

  /** The log. */
  private Log log;

  /**
   * Inits the.
   *
   * @param log the log
   */
  @Inject
  public void init( Log log )
  {
    this.log = log;
  }

  /**
   * Instantiates a new single sign on impl.
   *
   * @param ticket the ticket
   * @param spn the spn
   */
  public SingleSignOnImpl( byte[] ticket, String spn, Log log )
  {
    this.ticket = ticket;
    this.spn = spn;
    this.log = log;
  }

  /** {@inheritDoc} */
  @Override
  public String run() throws Exception
  {
    /**
     * Kerberos V5 Mechanism or SPNEGO required. the Legacy mechanism is NOT supported. SPNEGO
     * (1.3.6.1.5.5.2); Kerberos V5 (1.2.840.113554.1.2.2)
     */
    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 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 */
    log.info( "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();
    log.info( "request from Client {0}", clientName );

    /** clean up the context. This is very important */
    gssContext.dispose();

    return clientName;
  }

}

我的日志吐出这个:

AUTHTOKEN: YIIG2gYGKw......
2016-11-08 14:52:34,269 INFO Entering accpetSecContext...
2016-11-08 14:52:34,269 ERROR Invalid ticket for HTTP/ping01.cool.domain@COOL.DOMAIN: java.security.PrivilegedActionException: GSSException: Failure unspecified at GSS-API level (Mechanism level: Checksum failed)
2016-11-08 14:52:34,269 INFO VALID: false

但好消息是我获得了 Kerberos token 。他进入上下文并可以解密它。在 cmd 中使用 klist 时,我什至看到了我的服务的缓存票证。如此处所示:directUpload

Active Directory 和 KDC 运行平稳,当我请求访问该服务的票证时没有显示错误或警告。

带token的byte[]取自httprequest的header,用Base64.getMimeDecoder.decode()从字符串解码成byte[];

有没有人看出我的错误?重新启动并清除票证缓存后,我仍然收到消息。

编辑 2:

我深入挖掘并尝试使用 ktab 和 kinit。在尝试使用 kinit 获取 SPN 的票时,我遇到了这个问题:

kinit -J-Dsun.security.krb5.debug=true -k -t http_ping01.ktab HTTP/ping01.cool.domain@COOL.DOMAIN

>>>KinitOptions cache name is C:\Users\Nico.DOMAIN\XXXX_Nico
Principal is HTTP/ping01.cool.domain@COOL.DOMAIN

>>> Kinit using keytab
>>> Kinit keytab file name: http_ping01.ktab
Java config name: null

LSA: Found Ticket
LSA: Made NewWeakGlobalRef
LSA: Found PrincipalName
LSA: Made NewWeakGlobalRef
LSA: Found DerValue
LSA: Made NewWeakGlobalRef
LSA: Found EncryptionKey
LSA: Made NewWeakGlobalRef
LSA: Found TicketFlags
LSA: Made NewWeakGlobalRef
LSA: Found KerberosTime
LSA: Made NewWeakGlobalRef
LSA: Found String
LSA: Made NewWeakGlobalRef
LSA: Found DerValue constructor
LSA: Found Ticket constructor
LSA: Found PrincipalName constructor
LSA: Found EncryptionKey constructor
LSA: Found TicketFlags constructor
LSA: Found KerberosTime constructor
LSA: Finished OnLoad processing

Native config name: C:\Windows\krb5.ini
Loaded from native config
>>> Kinit realm name is COOL.DOMAIN
>>> Creating KrbAsReq
>>> KrbKdcReq local addresses for my_computer are:

[My addresses]

>>> KdcAccessibility: reset
>>> KeyTabInputStream, readName(): COOL.DOMAIN
>>> KeyTabInputStream, readName(): HTTP
>>> KeyTabInputStream, readName(): ping01.cool.domain
>>> KeyTab: load() entry length: 98; type: 18

>>> KeyTabInputStream, readName(): COOL.DOMAIN
>>> KeyTabInputStream, readName(): HTTP
>>> KeyTabInputStream, readName(): ping01.cool.domain
>>> KeyTab: load() entry length: 82; type: 17

>>> KeyTabInputStream, readName(): COOL.DOMAIN
>>> KeyTabInputStream, readName(): HTTP
>>> KeyTabInputStream, readName(): ping01.cool.domain
>>> KeyTab: load() entry length: 82; type: 23

>>> KeyTabInputStream, readName(): COOL.DOMAIN
>>> KeyTabInputStream, readName(): HTTP
>>> KeyTabInputStream, readName(): ping01.cool.domain
>>> KeyTab: load() entry length: 90; type: 16

>>> KeyTabInputStream, readName(): COOL.DOMAIN
>>> KeyTabInputStream, readName(): HTTP
>>> KeyTabInputStream, readName(): ping01
>>> KeyTab: load() entry length: 80; type: 18

>>> KeyTabInputStream, readName(): COOL.DOMAIN
>>> KeyTabInputStream, readName(): HTTP
>>> KeyTabInputStream, readName(): ping01
>>> KeyTab: load() entry length: 64; type: 17

>>> KeyTabInputStream, readName(): COOL.DOMAIN
>>> KeyTabInputStream, readName(): HTTP
>>> KeyTabInputStream, readName(): ping01
>>> KeyTab: load() entry length: 64; type: 23

>>> KeyTabInputStream, readName(): COOL.DOMAIN
>>> KeyTabInputStream, readName(): HTTP
>>> KeyTabInputStream, readName(): ping01
>>> KeyTab: load() entry length: 72; type: 16

Looking for keys for: HTTP/ping01.cool.domain@COOL.DOMAIN
Added key: 16version: 2
Added key: 23version: 2
Added key: 17version: 2
Added key: 18version: 2
default etypes for default_tkt_enctypes: 18 17 23 16.

>>> KrbAsReq creating message
>>> KrbKdcReq send: kdc=KDC.cool.domain UDP:88, timeout=30000, number of retries =3, #bytes=270
>>> KDCCommunication: kdc=KDC.cool.domain UDP:88, timeout=30000,Attempt =1, #bytes=270
>>> KrbKdcReq send: #bytes read=106
>>> KdcAccessibility: remove KDC.cool.domain
>>> KDCRep: init() encoding tag is 126 req type is 11

>>>KRBError:
         sTime is Wed Nov 09 10:54:46 CET 2016 1478685286000
         suSec is 578393
         error code is 6
         error Message is Client not found in Kerberos database
         sname is ActiveDirectory/COOL.DOMAIN@COOL.DOMAIN
         msgType is 30
Exception: krb_error 6 Client not found in Kerberos database (6) Client not found in Kerberos database

KrbException: Client not found in Kerberos database (6)
        at sun.security.krb5.KrbAsRep.<init>(KrbAsRep.java:76)
        at sun.security.krb5.KrbAsReqBuilder.send(KrbAsReqBuilder.java:316)
        at sun.security.krb5.KrbAsReqBuilder.action(KrbAsReqBuilder.java:361)
        at sun.security.krb5.internal.tools.Kinit.<init>(Kinit.java:219)
        at sun.security.krb5.internal.tools.Kinit.main(Kinit.java:113)
Caused by: KrbException: Identifier doesn't match expected value (906)
        at sun.security.krb5.internal.KDCRep.init(KDCRep.java:140)
        at sun.security.krb5.internal.ASRep.init(ASRep.java:64)
        at sun.security.krb5.internal.ASRep.<init>(ASRep.java:59)
        at sun.security.krb5.KrbAsRep.<init>(KrbAsRep.java:60)
        ... 4 more

所以我查看了 Activity 目录中的对象 ping01。它已经有一堆 servicePrincipalName 属性:

servicePrincipalName: someService/PING01.cool.domain

servicePrincipalName: someService/PING01

servicePrincipalName: anotherService/PING01.cool.domain

servicePrincipalName: anotherService/PING01

servicePrincipalName: HOST/PING01.cool.domain

servicePrincipalName: HOST/PING01

使用 setspn -l mgmt 输出我创建的 SPN。只是在 ldapBrowser 中根本不可见。

我不确定对象 Ping01 (objectClass=computer) 是否有密码,必须等待系统管理员的回答。

编辑 3: 我发现这一定是某种 SPN 问题或至少是 AD 问题。 从 EDIT 2: 你可以看到,即使是 windows 本地工具 kinit 也无法执行身份验证,因为 kdc 正在发送他不认识用户的消息。为什么他以用户身份向 SPN 声明我不清楚,但打开更多调试选项给了我这个输出:

YIIG2gYGKwYBBQUCoIIGzjCCBsqgMDAuBgkqhkiC9xIBAgIGCSqGSIb3EgECAgYKKwYBBAGCNwICHgYKKwYBBAGCNwICCqKCBpQEggaQYIIGjAYJKoZIhvcSAQICAQBuggZ7MIIGd6ADAgEFoQMCAQ6iBwMFACAAAACjggUGYYIFAjCCBP6gAwIBBaETGxFGRUxURU5HUk9VUC5MT0NBTKIrMCmgAwIBAqEiMCAbBEhUVFAbGHBpbmcwMS5mZWx0ZW5ncm91cC5sb2NhbKOCBLMwggSvoAMCAR[...]
INFO [stdout] Debug is true storeKey true useTicketCache false useKeyTab true doNotPrompt true ticketCache is null isInitiator false KeyTab is C:\path\to\http_ping01_test.ktab refreshKrb5Config is false principal is HTTP/ping01.cool.domain@COOL.DOMAIN tryFirstPass is false useFirstPass is false storePass is false clearPass is false
INFO [stdout] principal is HTTP/ping01.cool.domain@COOL.DOMAIN
INFO [stdout] Will use keytab
INFO [stdout] Commit Succeeded
INFO [stdout] Found KeyTab C:\path\to\http_ping01_test.ktab for HTTP/ping01.cool.domain@COOL.DOMAIN
INFO Entering accpetSecContext...
INFO [stdout] Entered SpNegoContext.acceptSecContext with state=STATE_NEW
INFO [stdout] SpNegoContext.acceptSecContext: receiving token = a0 82 06 ce 30 82 06 ca a0 30 30 2e 06 09 2a 86 48 82 f7 12 01 02 02 06 09 2a 86 48 86 f7 12 01 02 02 06 0a 2b 06 01 04 01 82 37 02 02 1e 06 0a 2b 06 01 04 01 82 37 02 02 0a a2 82 06 94 04 82 06 90 60 82 06 8c 06 09 2a 86 48 86 f7 12 01 02 02 01 00 6e 82 06 7b 30 82 06 77 a0 03 02 01 05 a1 03 02 01 0e a2 07 03 05 00 20 00 00 00 a3 82 05 06 61 82 05 02 30 82 04 fe a0 03 02 01 05 a1 13 1b 11 46 45 4c 54 45 4e 47 52 4f 55 50 2e 4c 4f 43 41 4c a2 2b 30 29 a0 03 02 01 02 a1 22 30 20 1b 04 48 54 54 50 1b 18 70 69 6e 67 30 31 2e 66 65 6c 74 65 6e 67 72 6f 75 70 2e 6c 6f 63 61 6c a3 82 04 b3 30 82 04 af a0 03 02 01 12 a1 03 02 01 0f a2 82 04 a1 04 82 04 9d e6 c0 24 8d 0d 24 8e e1 4e e8 0d 4e 4d 5b 7e 06 58 d9 f2 04 a6 99 55 e2 61 67 99 60 ec 47 42 7d 60 64 4d bc f7 ef 99 5b f0 3e b8 2f 9a ff 2d 83 19 6d f1 5f ac 44 08 f3 50 d5 c9 53 af 6f d9 d6 81 c1 d7 24 03 6a 9d b4 9d 56 53 93 b3 1d 07 15 77 c5 fb 25 0f bc f8 97 8f 97 0c 26 ae 52 d0 fc f3 72 98 9c 79 4b af e2 88 3b a6 2b 1b 03 b0 93 b6 6a dd b3 c6 f8 c2 01 eb a4 1b 8a 64 74 cb 5b f4 4b 5c d7 02 48 1d 0d 5e 29 3d 2b 82 c5 79 a1 7a e1 4c 92 32 7c 6b f6 56 ff e1 3a 3f b7 ce 0c 92 f8 ae ce 03 f2 f5 18 53 5c 5b 08 07 60 d7 c0 38 7d d0 f5 fa 2b 63 97 61 75 86 b6 95 44 49 76 93 38 88 82 7f 90 07 d7 3d c9 bd c6 c7 b3 af 47 55 cc b0 1a cd 2a e8 4e d0 b9 42 9e 65 3e aa 88 ac b5 25 45 39 20 0f 3c 50 ed 2d 1a f5 24 04 5a 15 99 c9 2e c1 c6 40 4e 26 ea f2 c6 a9 bd 61 24 fc d4 25 6e ed c2 40 3a d6 18 9b 53 ac 4d a1 61 d2 12 aa 99 e1 90 6e 22 c9 14 82 49 78 43 ab 83 a1 60 a3 d0 1d 33 24 11 41 07 4d bb 9c 0e 38 e1 3c 86 6a 62 bc 2f 7c 47 34 b7 42 3e 28 2e 9b 26 66 a1 e8 61 5f 00 61 8a b9 2b 5b 9e b2 aa 1a 4d e7 4e d2 6d 52 e1 25 c4 89 ea 6e 85 1c 1a 56 e0 d9 a2 be 9f 7c ee 89 55 b8 39 cf b9 92 77 33 2d fa 64 29 50 38 2d 6d d7 9d be be 3c e2 04 4c 5c 3e 3b d1 09 39 08 bd 75 5b 9f 6a 89 32 f8 b2 a9 c7 a3 a1 de ca ea fd 62 18 7d df 5e 50 b5 8e 48 71 ec 66 70 ff 0e 1c 40 2a ad 9e f4 c4 15 45 ca 1b 15 b8 0e 30 76 76 9b 81 39 5b 94 c4 0a ec e0 a7 b4 ec 32 9a 4a 9d 74 86 a3 81 5a 91 8c 51 e1 5a f1 b8 44 fa 9d cc 16 34 c5 99 fb 7b 33 bc 06 99 51 9e ec 19 60 88  [...]
INFO [stdout] SpNegoToken NegTokenInit: reading Mechanism Oid = 1.2.840.48018.1.2.2
INFO [stdout] SpNegoToken NegTokenInit: reading Mechanism Oid = 1.2.840.113554.1.2.2
INFO [stdout] SpNegoToken NegTokenInit: reading Mechanism Oid = 1.3.6.1.4.1.311.2.2.30
INFO [stdout] SpNegoToken NegTokenInit: reading Mechanism Oid = 1.3.6.1.4.1.311.2.2.10
INFO [stdout] SpNegoToken NegTokenInit: reading Mech Token
INFO [stdout] SpNegoContext.acceptSecContext: received token of type = SPNEGO NegTokenInit
INFO [stdout] SpNegoContext: negotiated mechanism = 1.2.840.113554.1.2.2
INFO [stdout] SpNegoContext.acceptSecContext: negotiated mech adjusted to 1.2.840.48018.1.2.2
INFO [stdout] Entered Krb5Context.acceptSecContext with state=STATE_NEW
INFO [stdout] Looking for keys for: HTTP/ping01.cool.domain@COOL.DOMAIN
INFO [stdout] Added key: 16version: 2
INFO [stdout] Added key: 23version: 2
INFO [stdout] Added key: 17version: 2
INFO [stdout] Added key: 18version: 2
INFO [stdout] >>> EType: sun.security.krb5.internal.crypto.Aes256CtsHmacSha1EType
ERROR Invalid ticket for HTTP/ping01.cool.domain@COOL.DOMAIN: java.security.PrivilegedActionException: GSSException: Failure unspecified at GSS-API level (Mechanism level: Checksum failed)
INFO [stdout] [Krb5LoginModule]: Entering logout
INFO [stdout] [Krb5LoginModule]: logged out Subject

最佳答案

安全 token 的实际检查过程 - 包含 Kerberos 票证 - 发生在您的应用程序服务器上 - 它永远不会联系 AD。 GSSAPI 安全函数处理这个——您不需要为此编写代码。您可以公开 token (看起来像一串随机的字母),但只有 key 表才能对其进行解密。当您(作为应用程序服务器)从用户那里获得 Kerberos 票证(身份验证 token )时,您知道该用户是合法的 - 用户首先不会获得票证,除非他们的身份已经被 AD 证明 - 就是这样Kerberos 有效。查看此 URL 以获取更多信息:http://docs.oracle.com/javase/7/docs/technotes/guides/security/jgss/single-signon.html

我根据您编辑的问题做出的一些新观察:

  1. SPN 错误。您提供的格式“SPN HTTP/ping01.domain@DOMAIN”缺少两个标签,一个在中间的主机部分,一个在最后的领域部分。如果您打算那样使用它,应该类似于“HTTP/ping01.domain.com@DOMAIN.COM”。正确运行 DNS 并正确配置 krb5.conf 后,SPN 实际上可以是这样的:“HTTP/ping01.domain.com”。

像这样添加新的 SPN:

setspn -S HTTP/ping01.domain.com 域\帐号

(setspn -S 在添加之前查找重复项,而 setspn -A 现在执行)

  1. krb5.conf 中有错误。你有:

    default_realm = DOMAIN

应该是:

default_realm = DOMAIN.COM

在 krb5.conf 的下方,在 [realms] 和 {domain_realm] 下,所有名称引用也需要完全限定。

  1. 这也适用于您的代码块中的相同引用,我注意到其中缺少标签的名称,例如这个:

    getJaasKrb5TicketCfg( "HTTP/ping01.domain@DOMAIN", "DOMAIN",

应该是:

getJaasKrb5TicketCfg( "HTTP/ping01.domain.com@DOMAIN.COM", "DOMAIN.COM",

Kerberos 非常依赖 DNS,所有名称引用都应该在所有代码和 krb5.conf 中完全限定。

关于java - 如何验证kerberos token ?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/40216647/

相关文章:

c++ - 对服务器/客户端应用程序使用 Kerberos 身份验证

java - 我应该使用什么方法将 int 转换为 String?

java - 在 Eclipse Helios 中使用 C++ 的 Android Cocos2DX

java - 如何测试重定向样式API

c++ - 如何使用 C++ Win32API 检查 ADuser "Password never expires"属性检查或不检查?

java - commons http 客户端 - 协商时的 kerberos token 有\r\n(回车换行)字符

java - 淡褐色 : Maps and Set

.net - 您能计算出 Active Directory 使用的密码哈希吗?

powershell - 如何从 Get-ADComputer 结果中排除特定名称?

macos - OSX 上默认的 kerberos 凭证缓存是什么?