IT点滴

我不去想是否能够成功 既然选择了远方 便只顾风雨兼程
  博客园  :: 首页  :: 联系 :: 订阅 订阅  :: 管理

NTLM SSO的实现

Posted on 2007-11-28 10:27  Ady Lee  阅读(5670)  评论(2编辑  收藏  举报

最近项目中要求实现Web应用的SSOSingle Sign On),即对于已经登录到Windows Domain中的用户,不需要输入用户名、密码而直接使用当前登录的Domain用户信息进行验证,如果验证成功则进入,否则拒绝进入。

在网上搜了一些资料,同时也对NTLM的认证方式有了些了解,记录之。

NTLM HTTP认证

过程如下:

    1: C  --> S   GET ...

    2: C <--  S   401 Unauthorized

                 WW-Authenticate: NTLM

    3: C  --> S   GET ...

                 Authorization: NTLM <base64-encoded type-1-message>

    4: C <--  S   401 Unauthorized

                 WWW-Authenticate: NTLM <base64-encoded type-2-message>

    5: C  --> S   GET ...

                 Authorization: NTLM <base64-encoded type-3-message>

    6: C <--  S   200 Ok

从交互过程可以发现,client会发送type-1消息和type-3消息给server,而server会发送type-2消息给client

Type-1消息包括机器名、Domain

Type-2消息包括server发出的NTLM challenge

Type-3消息包括用户名、机器名、Domain、以及两个根据server发出的challenge计算出的response,这里response是基于challenge和当前用户的登录密码计算而得

具体细节参考下面两个网址:

http://www.innovation.ch/personal/ronald/ntlm.html

http://davenport.sourceforge.net/ntlm.html#whatIsNtlm

注:

IE里,上述的交互会由浏览器自动完成,M$总是有办法自己到OS里去拿到Domain、用户名、密码等等信息的,而FF就没有这么方便了,它必须要用户手工输入,当server返回401错误后,FF会弹出该对话框让用户输入用户名、密码(在IE中,如果使用当前登录的用户名、密码验证失败后也会弹出这样的对话框)

OK,有了NTLM HTTP认证协议,下面要实现SSO就方便多了。这时server已经拿到client的认证信息:用户名、Domain、密码和challenge的某个运算值,这时server只要利用这些信息连接到ADActive Directory,活动目录)(或者其他认证服务器)进行认证即可。

但这里还有个问题,因为server拿到的并不是密码,而是密码的某个单向hash值,那怎么用这个信息到AD上认证呢?

答案是SMBServer Message Block)!

SMBM$用来进行局域网文件共享和传输的协议,也称为CIFSCommon Internet File System),CIFS协议的细节可以在MSDN上查到:

http://msdn2.microsoft.com/en-us/library/aa302240.aspx

也可以到samba上去看看最新的一些发展:

http://www.samba.org/

我们着重看一下CIFS协议里连接和断开连接的部分:

连接:

断开连接:

OK,看起来蛮复杂的,不过没关系,关键我们要知道,在CIFS连接server(比如AD)时,首先server会发一个叫做EncryptionKey的东东给client,然后client会利用和NTLM HTTP认证中一样的算法计算出一个responseserver,这个细节很关键!

因为如果http server(在这里充当CIFSclient)用这个EncryptionKey作为给http clientchallengehttp client会计算出responsehttp server,然后http server就可以拿着这个responseAD上验证了!

现在有三个参与者了:http clienthttp serverAD

想象一下,首先http clienthttp请求给http server,为了对这个client认证,http server首先连接AD,然后就得到一个EncryptionKey,它就把这个EncryptionKey作为challenge返回给http client,然后http client会根据这个challenge和用户密码计算出response送给http server,而http server就拿着这个responseAD去认证了J

下图就表示整个这个过程:

现在,我们已经有足够的理论武装起来可以实现SSO了,但是,难道要我们自己去实现这些协议吗?当然可以,有兴趣可以尝试一下J

不过另一个选择是使用Open SourcelibraryjCIFS就是干这些事情的。

jCIFSsamba组织下的一帮牛开发的一套兼容SMB协议的library,我们可以用它来在java里访问Windows共享文件,当然,既然它帮我们实现了SMB协议,那要用它来实现NTLM SSO就很容易了。

http://jcifs.samba.org/

在这个网址可以下载到jCIFSsource codelibrary

好,现在可以休息一下了,我们通过一个例子step by step看一下jCIFS怎么来实现SSO吧。

1.       jcifs-1.2.13.jar放到tomcatwebapp目录

2.       创建一个web.xml,用于创建一个servlet filter,处理http连接(记得把里面的ip地址替换为你自己的AD serverip地址)

<web-app xmlns="http://java.sun.com/xml/ns/javaee"

   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

   xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"

   version="2.5">

  <display-name>Welcome to Tomcat</display-name>

  <description>

     Welcome to Tomcat

  </description>

  <filter>

    <filter-name>NtlmHttpFilter</filter-name>

    <filter-class>jcifs.http.NtlmHttpFilter</filter-class>

    <init-param>

        <param-name>jcifs.http.domainController</param-name>

        <param-value>10.28.1.212</param-value>

    </init-param>

    <init-param>

    <param-name>jcifs.util.loglevel</param-name>

    <param-value>6</param-value>

    </init-param>

  </filter>

  <filter-mapping>

    <filter-name>NtlmHttpFilter</filter-name>

    <url-pattern>/*</url-pattern>

  </filter-mapping>

</web-app>

3.       重新启动tomcat,打开http://localhost:8080/,如果用的IE,就会自动使用当前用户进行验证,而如果使用FF,就会弹出对话框,输入用户名密码后就可以验证通过,看到tomcat的页面了

这个例子够简单的,jCIFS应用也确实非常简单了,当然如果你要实现一些其他特性,比如根据当前登录的用户账户决定用户的权限、以及看到页面的内容,那你就必须通过jCIFSAPI去操作了,可以参考jCIFSAPI文档:

http://jcifs.samba.org/src/docs/api/

最后,说点这个方案的问题和不足吧,

-          首先由于jCIFS只是应用了SMB协议进行认证,这样它就没办法拿到用户的其他的一些信息,比如组信息或者权限信息。对于这个问题,一般可以由我们自己的应用程序通过LDAPAD上去存取,但毕竟增加了我们的工作。

-          第二个不足是,NTLM认证是一个M$准备放弃的协议,在Windows 2000和以后的操作系统中,缺省的认证协议是Kerberos,只有在和2000之前的系统通信时才使用NTLM。当然这并不是说jCIFS2000以上就用不起来了,缺省情况总是可以用的,M$总是要保持兼容的J当然如果你想实现基于KerberosSSO,你可以去参考下面列出的文章,但这就不是这里讨论的话题了。

http://free.tagish.net/jaas/

http://java.sun.com/j2se/1.4.2/docs/guide/security/jgss/single-signon.html

附录部分给出NTLM协议和算法的细节,不感兴趣的就不用管它了,反正这些会由client(一般是IEFF)和jCIFS已经帮我们处理了。

Type-1消息格式

struct {

    byte    protocol[8];     // 'N', 'T', 'L', 'M', 'S', 'S', 'P', '"0'

    byte    type;            // 0x01

    byte    zero[3];

    short   flags;           // 0xb203

    byte    zero[2];

    short   dom_len;         // domain string length

    short   dom_len;         // domain string length

    short   dom_off;         // domain string offset

    byte    zero[2];

    short   host_len;        // host string length

    short   host_len;        // host string length

    short   host_off;        // host string offset (always 0x20)

    byte    zero[2];

    byte    host[*];         // host string (ASCII)

    byte    dom[*];          // domain string (ASCII)

} type-1-message;

Type-2消息格式

struct {

    byte    protocol[8];     // 'N', 'T', 'L', 'M', 'S', 'S', 'P', '"0'

    byte    type;            // 0x02

    byte    zero[7];

    short   msg_len;         // 0x28

    byte    zero[2];

    short   flags;           // 0x8201

    byte    zero[2];

    byte    nonce[8];        // nonce

    byte    zero[8];

} type-2-message;

Type-3消息格式

struct {

    byte    protocol[8];     // 'N', 'T', 'L', 'M', 'S', 'S', 'P', '"0'

    byte    type;            // 0x03

    byte    zero[3];

    short   lm_resp_len;     // LanManager response length (always 0x18)

    short   lm_resp_len;     // LanManager response length (always 0x18)

    short   lm_resp_off;     // LanManager response offset

    byte    zero[2];

    short   nt_resp_len;     // NT response length (always 0x18)

    short   nt_resp_len;     // NT response length (always 0x18)

    short   nt_resp_off;     // NT response offset

    byte    zero[2];

    short   dom_len;         // domain string length

    short   dom_len;         // domain string length

    short   dom_off;         // domain string offset (always 0x40)

    byte    zero[2];

    short   user_len;        // username string length

    short   user_len;        // username string length

    short   user_off;        // username string offset

    byte    zero[2];

    short   host_len;        // host string length

    short   host_len;        // host string length

    short   host_off;        // host string offset

    byte    zero[6];

    short   msg_len;         // message length

    byte    zero[2];

    short   flags;           // 0x8201

    byte    zero[2];

    byte    dom[*];          // domain string (unicode UTF-16LE)

    byte    user[*];         // username string (unicode UTF-16LE)

    byte    host[*];         // host string (unicode UTF-16LE)

    byte    lm_resp[*];      // LanManager response

    byte    nt_resp[*];      // NT response

} type-3-message;

Response的计算算法

/* setup LanManager password */

char  lm_pw[14];

int   len = strlen(passw);

if (len > 14)  len = 14;

for (idx=0; idx<len; idx++)

    lm_pw[idx] = toupper(passw[idx]);

for (; idx<14; idx++)

    lm_pw[idx] = 0;

/* create LanManager hashed password */

unsigned char magic[] = { 0x4B, 0x47, 0x53, 0x21, 0x40, 0x23, 0x24, 0x25 };

unsigned char lm_hpw[21];

des_key_schedule ks;

setup_des_key(lm_pw, ks);

des_ecb_encrypt(magic, lm_hpw, ks);

setup_des_key(lm_pw+7, ks);

des_ecb_encrypt(magic, lm_hpw+8, ks);

memset(lm_hpw+16, 0, 5);

/* create NT hashed password */

int   len = strlen(passw);

char  nt_pw[2*len];

for (idx=0; idx<len; idx++)

{

    nt_pw[2*idx]   = passw[idx];

    nt_pw[2*idx+1] = 0;

}

unsigned char nt_hpw[21];

MD4_CTX context;

MD4Init(&context);

MD4Update(&context, nt_pw, 2*len);

MD4Final(nt_hpw, &context);

memset(nt_hpw+16, 0, 5);

/* create responses */

unsigned char lm_resp[24], nt_resp[24];

calc_resp(lm_hpw, nonce, lm_resp);

calc_resp(nt_hpw, nonce, nt_resp);

Helpers:

/*

 * takes a 21 byte array and treats it as 3 56-bit DES keys. The

 * 8 byte plaintext is encrypted with each key and the resulting 24

 * bytes are stored in the results array.

 */

void calc_resp(unsigned char *keys, unsigned char *plaintext, unsigned char *results)

{

    des_key_schedule ks;

    setup_des_key(keys, ks);

    des_ecb_encrypt((des_cblock*) plaintext, (des_cblock*) results, ks, DES_ENCRYPT);

    setup_des_key(keys+7, ks);

    des_ecb_encrypt((des_cblock*) plaintext, (des_cblock*) (results+8), ks, DES_ENCRYPT);

    setup_des_key(keys+14, ks);

    des_ecb_encrypt((des_cblock*) plaintext, (des_cblock*) (results+16), ks, DES_ENCRYPT);

}

/*

 * turns a 56 bit key into the 64 bit, odd parity key and sets the key.

 * The key schedule ks is also set.

 */

void setup_des_key(unsigned char key_56[], des_key_schedule ks)

{

    des_cblock key;

    key[0] = key_56[0];

    key[1] = ((key_56[0] << 7) & 0xFF) | (key_56[1] >> 1);

    key[2] = ((key_56[1] << 6) & 0xFF) | (key_56[2] >> 2);

    key[3] = ((key_56[2] << 5) & 0xFF) | (key_56[3] >> 3);

    key[4] = ((key_56[3] << 4) & 0xFF) | (key_56[4] >> 4);

    key[5] = ((key_56[4] << 3) & 0xFF) | (key_56[5] >> 5);

    key[6] = ((key_56[5] << 2) & 0xFF) | (key_56[6] >> 6);

    key[7] =  (key_56[6] << 1) & 0xFF;

    des_set_odd_parity(&key);

    des_set_key(&key, ks);

}

 

Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=1694634