storage R&D guy.
全部博文(1000)
分类: 服务器与存储
2015-06-10 13:50:54
在网上搜了一些资料,同时也对 NTLM 的认证方式有了些了解,记录之。
NTLM HTTP 认证
过程如下:
1: C --> S GET ...
2: C <-- S 401 Unauthorized
WW-Authenticate: NTLM
3: C --> S GET ...
Authorization: NTLM
4: C <-- S 401 Unauthorized
WWW-Authenticate: NTLM
5: C --> S GET ...
Authorization: NTLM
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 和当前用户的登录密码计算而得
具体细节参考下面两个网址:
注:
在 IE 里,上述的交互会由浏览器自动完成, M$ 总是有办法自己到 OS 里去拿到 Domain 、用户名、密码等等信息的,而 FF 就没有这么方便了,它必须要用户手工输入,当 server 返回 401 错误后, FF 会弹出该对话框让用户输入用户名、密码(在 IE 中,如果使用当前登录的用户名、密码验证失败后也会弹出这样的对话框)
OK ,有了 NTLM HTTP 认证协议,下面要实现 SSO 就方便多了。这时 server 已经拿到 client 的认证信息:用户名、 Domain 、密码和 challenge 的某个运算值,这时 server 只要利用这些信息连接到 AD ( Active Directory ,活动目录)(或者其他认证服务器)进行认证即可。
但这里还有个问题,因为 server 拿到的并不是密码,而是密码的某个单向 hash 值,那怎么用这个信息到 AD 上认证呢?
答案是 SMB ( Server Message Block )!
SMB 是 M$ 用来进行局域网文件共享和传输的协议,也称为 CIFS ( Common Internet File System ), CIFS 协议的细节可以在 MSDN 上查到:
也可以到 samba 上去看看最新的一些发展:
/
我们着重看一下 CIFS 协议里连接和断开连接的部分:
连接:
断开连接:
OK ,看起来蛮复杂的,不过没关系,关键我们要知道,在 CIFS 连接 server (比如 AD )时,首先 server 会发一个叫做 EncryptionKey 的东东给 client ,然后 client 会利用和 NTLM HTTP 认证中一样的算法计算出一个 response给 server ,这个细节很关键!
因为如果 http server (在这里充当 CIFS 的 client )用这个 EncryptionKey 作为给 http client 的 challenge , http client 会计算出 response 给 http server ,然后 http server 就可以拿着这个 response 到 AD 上验证了!
现在有三个参与者了: http client , http server 和 AD
想象一下,首先 http client 发 http 请求给 http server ,为了对这个 client 认证, http server 首先连接 AD ,然后就得到一个 EncryptionKey ,它就把这个 EncryptionKey 作为 challenge 返回给 http client ,然后 http client 会根据这个 challenge 和用户密码计算出 response 送给 http server ,而 http server 就拿着这个 response 到 AD 去认证了 J
下图就表示整个这个过程:
现在,我们已经有足够的理论武装起来可以实现 SSO 了,但是,难道要我们自己去实现这些协议吗?当然可以,有兴趣可以尝试一下 J
不过另一个选择是使用 Open Source 的 library , jCIFS 就是干这些事情的。
jCIFS 是 samba 组织下的一帮牛开发的一套兼容 SMB 协议的 library ,我们可以用它来在 java 里访问 Windows 共享文件,当然,既然它帮我们实现了 SMB 协议,那要用它来实现 NTLM SSO 就很容易了。
在这个网址可以下载到 jCIFS 的 source code 和 library
好,现在可以休息一下了,我们通过一个例子 step by step 看一下 jCIFS 怎么来实现 SSO 吧。
1. 把 jcifs-1.2.13.jar 放到 tomcat 的 webapp 目录
2. 创建一个 web.xml ,用于创建一个 servlet filter ,处理 http 连接(记得把里面的 ip 地址替换为你自己的 AD server 的 ip 地址)
xmlns:xsi=""
xsi:schemaLocation=" /web-app_2_5.xsd"
version="2.5">
Welcome to Tomcat
3. 重新启动 tomcat ,打开 ,如果用的 IE ,就会自动使用当前用户进行验证,而如果使用 FF ,就会弹出对话框,输入用户名密码后就可以验证通过,看到 tomcat 的页面了
这个例子够简单的, jCIFS 应用也确实非常简单了,当然如果你要实现一些其他特性,比如根据当前登录的用户账户决定用户的权限、以及看到页面的内容,那你就必须通过 jCIFS 的 API 去操作了,可以参考 jCIFS 的 API 文档:
最后,说点这个方案的问题和不足吧,
- 首先由于 jCIFS 只是应用了 SMB 协议进行认证,这样它就没办法拿到用户的其他的一些信息,比如组信息或者权限信息。对于这个问题,一般可以由我们自己的应用程序通过 LDAP 到 AD 上去存取,但毕竟增加了我们的工作。
- 第二个不足是, NTLM 认证是一个 M$ 准备放弃的协议,在 Windows 2000 和以后的操作系统中,缺省的认证协议是 Kerberos ,只有在和 2000 之前的系统通信时才使用 NTLM 。当然这并不是说 jCIFS 在 2000 以上就用不起来了,缺省情况总是可以用的, M$ 总是要保持兼容的 J 当然如果你想实现基于 Kerberos 的 SSO ,你可以去参考下面列出的文章,但这就不是这里讨论的话题了。
http://java.sun.com/j2se/1.4.2/docs/guide/security/jgss/single-signon.html
附录部分给出 NTLM 协议和算法的细节,不感兴趣的就不用管它了,反正这些会由 client (一般是 IE 或 FF )和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
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
{
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);
}