HttpClient三种不同的服务器认证客户端方案

http://blog.csdn.net/i_lovefish/article/details/9816783

HttpClient三种不同的认证方案: Basic, Digest and NTLM. 这些方案可用于服务器或代理对客户端的认证,简称服务器认证或代理认证。

服务器认证

  HttpClient处理服务器认证几乎是透明的,仅需要开发人员提供登录信息(login credentials)。登录信息保存在HttpState类的实例中,可以通过 setCredentials(String realm, Credentials cred)和getCredentials(String realm)来获取或设置。注意,设定对非特定站点访问所需要的登录信息,将realm参数置为null. HttpClient内建的自动认证,可以通过HttpMethod类的setDoAuthentication(boolean doAuthentication)方法关闭,而且这次关闭只影响HttpMethod当前的实例。

 

  抢先认证(Preemptive Authentication)可以通过下述方法打开.

 

  client.getState().setAuthenticationPreemptive(true);

 

  在这种模式时,HttpClient会主动将basic认证应答信息传给服务器,即使在某种情况下服务器可能返回认证失败的应答,这样做主要是为了减少连接的建立。为使每个新建的 HttpState实例都实行抢先认证,可以如下设置系统属性。

 

  setSystemProperty(Authenticator.PREEMPTIVE_PROPERTY, "true");

 

  Httpclient实现的抢先认证遵循rfc2617.

代理认证

  除了登录信息需单独存放以外,代理认证与服务器认证几乎一致。用 setProxyCredentials(String realm, Credentials cred)和 getProxyCredentials(String realm)设、取登录信息。

 

  认证方案(authentication schemes)

 

  Basic

 

  是HTTP中规定最早的也是最兼容(?)的方案,遗憾的是也是最不安全的一个方案,因为它以明码传送用户名和密码。它要求一个UsernamePasswordCredentials实例,可以指定服务器端的访问空间或采用默认的登录信息。

 

  Digest

 

  是在HTTP1.1中增加的一个方案,虽然不如Basic得到的软件支持多,但还是有广泛的使用。Digest方案比Basic方案安全得多,因它根本就不通过网络传送实际的密码,传送的是利用这个密码对从服务器传来的一个随机数(nonce)的加密串。它要求一个UsernamePasswordCredentials实例,可以指定服务器端的访问空间或采用默认的登录信息。

NTLM认证

  这是HttpClient支持的最复杂的认证协议。它M$设计的一个私有协议,没有公开的规范说明。一开始由于设计的缺陷,NTLM的安全性比Digest差,后来经过一个ServicePack补丁后,安全性则比较Digest高。NTLM需要一个NTCredentials实例. 注意,由于NTLM不使用访问空间(realms)的概念,HttpClient利用服务器的域名作访问空间的名字。还需要注意,提供给NTCredentials的用户名,不要用域名的前缀 - 如: "adrian" 是正确的,而 "DOMAIN\adrian" 则是错的.

 

  NTLM认证的工作机制与basic和digest有很大的差别。这些差别一般由HttpClient处理,但理解这些差别有助避免在使用NTLM认证时出现错误。

 

  从HttpClientAPI的角度来看,NTLM与其它认证方式一样的工作,差别是需要提供'NTCredentials'实例而不是'UsernamePasswordCredentials',对NTLM认证,访问空间是连接到的机器的域名,这对多域名主机会有一些麻烦.只有HttpClient连接中指定的域名才是认证用的域名。建议将realm设为null以使用默认的设置。

 

  NTLM只是认证了一个连接而不是一请求,所以每当一个新的连接建立就要进行一次认证,且在认证的过程中保持连接是非常重要的。 因此,NTLM不能同时用于代理认证和服务器认证,也不能用于http1.0连接或服务器不支持持久连接的情况。

Http基本认证

这是一个用于web浏览器或其他客户端在请求时提供用户名和密码的登录认证,要实现这个认证很简单:

我们先来看下协议里面怎么定义这个认证的. 1. 编码: 将用户名 追加一个 冒号(':')接上密码,把得出的结果字符串在用Base64算法编码.

  1. 请求头: Authorization: 认证类型 编码字符串

来看一下客户端如何发起请求例如,有一个用户名为:tom, 密码为:123456 怎么认证呢?

步骤如下 1. 编码

Base64('tom:123456') == dG9tOjEyMzQ1Ng==;

  1. 把编码结果放到请求头当中

    Authorization: Basic dG9tOjEyMzQ1Ng==

请求样例客户端

1
2
3
GET / HTTP/1.1
Host: localhost
Authorization: Basic dG9tOjEyMzQ1Ng

服务端应答

1
2
3
4
HTTP/1.1 200 OK
Date: Thu, 13 Jun 2013 20:25:37 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 53

如果没有认证信息

1
2
3
HTTP/1.1 401 Authorization Required
Date: Thu, 13 Jun 2013 20:25:37 GMT
WWW-Authenticate: Basic realm="Users"

验证失败的时候,响应头加上WWW-Authenticate: Basic realm="请求域".

这种http 基本实现,几乎目前所有浏览器都支持.不过,大家可以发现,直接把用户名和密码只是进行一次base64 编码实际上是很不安全的,因为对base64进行反编码十分容易,所以这种验证虽然简便,但是很少会在公开访问的互联网使用,一般多用在小的私有系统,例如,你们家里头的路由器,多用这种认证方式.

Http 摘要认证

这个认证可以看做是基本认证的增强版本,使用随机数+密码进行md5,防止通过直接的分析密码MD5防止破解. 摘要访问认证最初由 RFC 2069 (HTTP的一个扩展:摘要访问认证)中被定义加密步骤:

  1. HttpClient三种不同的服务器认证客户端方案

  2. HttpClient三种不同的服务器认证客户端方案

  3. HttpClient三种不同的服务器认证客户端方案

后来发现,就算这样还是不安全(md5 可以用彩虹表进行攻击),所以在RFC 2617入了一系列安全增强的选项;“保护质量”(qop)、随机数计数器由客户端增加、以及客户生成的随机数。这些增强为了防止如选择明文攻击的密码分析。

HttpClient三种不同的服务器认证客户端方案

  1. 如果 qop 值为“auth”或未指定,那么 HA2 为

    HttpClient三种不同的服务器认证客户端方案

  2. 如果 qop 值为“auth-int”,那么 HA2 为

    HttpClient三种不同的服务器认证客户端方案

  3. 如果 qop 值为“auth”或“auth-int”,那么如下计算 response:

    HttpClient三种不同的服务器认证客户端方案

  4. 如果 qop 未指定,那么如下计算 response:

    HttpClient三种不同的服务器认证客户端方案

好了,知道加密步骤,下面我们用文字来描述一下;

最后,我们的response 由三步计算所得. 1. 对用户名、认证域(realm)以及密码的合并值计算 MD5 哈希值,结果称为 HA1。

HA1 = MD5( "tom:Hi!:123456" ) = d8ae91c6c50fabdac442ef8d6a68ae8c

  1. 对HTTP方法以及URI的摘要的合并值计算 MD5 哈希值,例如,"GET" 和 "/index.html",结果称为 HA2。

    HA2 = MD5( "GET:/" ) = 71998c64aea37ae77020c49c00f73fa8

  2. 最后生成的响应码

    Response = MD5("d8ae91c6c50fabdac442ef8d6a68ae8c:L4qfzASytyQJAC2B1Lvy2llPpj9R8Jd3:00000001:c2dc5b32ad69187a
    :auth:71998c64aea37ae77020c49c00f73fa8") = 2f22e6d56dabb168702b8bb2d4e72453;

RFC2617 的安全增强的主要方式:

发起请求的时候,服务器会生成一个密码随机数(nonce)(而这个随机数只有每次"401"相应后才会更新),为了防止攻击者可以简单的使用同样的认证信息发起老的请求,于是,在后续的请求中就有一个随机数计数器(cnonce),而且每次请求必须必前一次使用的打.这样,服务器每次生成新的随机数都会记录下来,计数器增加.在RESPONSE 码中我们可以看出计数器的值会导致不同的值,这样就可以拒绝掉任何错误的请求.

请求样例(服务端 qop 设置为"auth")

客户端 无认证

1
2
GET / HTTP/1.1
Host: localhost

服务器响应(qop 为 'auth')

1
2
3
HTTP/1.1 401 Authorization Required
Date: Thu, 13 Jun 2013 20:25:37 GMT
WWW-Authenticate: Digest realm="Hi!", nonce="HSfb5dy15hKejXAbZ2VXjVbgNC8sC1Gq", qop="auth"

客户端请求(用户名: "tom", 密码 "123456")

1
2
3
4
5
6
7
8
9
GET / HTTP/1.1
Host: localhost
Authorization: Digest username="tom",
realm="Hi!",
nonce="L4qfzASytyQJAC2B1Lvy2llPpj9R8Jd3",
uri="/",
qop=auth,
nc=00000001,
cnonce="c2dc5b32ad69187a", response="2f22e6d56dabb168702b8bb2d4e72453"

服务端应答

1
2
3
4
HTTP/1.1 200 OK
Date: Thu, 13 Jun 2013 20:25:37 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 53

注意qop 设置的时候慎用:auth-int,因为一些常用浏览器和服务端并没有实现这个协议.

http协议的认证模式
http 的认证模式
  SIP类似Http协议。其认证模式也一样。Http协议(RFC 2616 )规定可以采用Base模式和摘要模式(Digest schema)。RFC 2617 专门对两种认证模式做了规定。RFC 1321 是MD5标准。Digest对现代密码破解来说并不强壮,但比基本模式还是好很多。MD5已经被山东大学教授找到方法可以仿冒(我的理解),但现在还在广泛使用。
1.最简单的攻击方式
  如果网站要求认证,客户端发送明文的用户名密码,那网络上的窃听者可以轻而易举的获得用户名密码,起不到安全作用。我上学时曾在科大实验室局域网内窃听别人的科大BBS的密码,发现BBS的用户名密码居然是明文传输的。那种做贼的心虚和做贼的兴奋让人激动莫名。偷人钱财会受到道德谴责,偷人密码只会暗自得意忘形。比“窃书不算偷”还没有罪恶感。因此你的用户名和密码明文传输的话,无异将一块肥肉放在嘴馋的人面前。现在很多ASP网站的认证都将用户名和密码用MD5加密。MD5是将任意长度的字符串和128位的随机数字运算后生成一个16byte的加密字符串。因此窃听者抓住的是一团乱码。但是,这有一个问题:如果窃听者就用这团乱码去认证,还是可以认证通过。因为服务器将用户名密码MD5加密后得到的字符串就是那一团乱码,自然不能区别谁是合法用户。这叫重放攻击(replay attack)。这和HTTP的基本认证模式差不多。为了安全,不要让别人不劳而获,自然要做基本的防范。下面是Http协议规定的两种认证模式。
2.基本认证模式
  客户向服务器发送请求,服务器返回401(未授权),要求认证。401消息的头里面带了挑战信息。realm用以区分要不同认证的部分。客户端收到401后,将用户名密码和挑战信息用BASE64加密形成证书,发送回服务器认证。语法如下:
      challenge   = "Basic" realm
      credentials = "Basic" basic-credentials
示例:
   认证头: WWW-Authenticate: Basic realm="zhouhh@mydomain.com"
   证书:Authorization: Basic QsdfgWGHffuIcaNlc2FtZQ==
3.摘要访问认证
  为了防止重放攻击,采用摘要访问认证。在客户发送请求后,收到一个401(未授权)消息,包含一个Challenge。消息里面有一个唯一的字符串:nonce,每次请求都不一样。客户将用户名密码和401消息返回的挑战一起加密后传给服务器。这样即使有窃听,他也无法通过每次认证,不能重放攻击。Http并不是一个安全的协议。其内容都是明文传输。因此不要指望Http有多安全。
语法:
      challenge        =  "Digest" digest-challenge
      digest-challenge  = 1#( realm | [ domain ] | nonce |
                          [ opaque ] |[ stale ] | [ algorithm ] |
                          [ qop-options ] | [auth-param] )
      domain            = "domain" "="  URI ( 1*SP URI ) 
      URI               = absoluteURI | abs_path
      nonce             = "nonce" "=" nonce-value
      nonce-value       = quoted-string
      opaque            = "opaque" "=" quoted-string
      stale             = "stale" "=" ( "true" | "false" )
      algorithm         = "algorithm" "=" ( "MD5" | "MD5-sess" |
                           token )
      qop-options       = "qop" "="  1#qop-value 
      qop-value         = "auth" | "auth-int" | token
realm:让客户知道使用哪个用户名和密码的字符串。不同的领域可能密码不一样。至少告诉用户是什么主机做认证,他可能会提示用哪个用户名登录,类似一个Email。
domain:一个URI列表,指示要保护的域。可能是一个列表。提示用户这些URI采用一样的认证。如果为空或忽略则为整个服务器。
nonce:随机字符串,每次401都不一样。跟算法有关。算法类似Base64加密:time-stamp H(time-stamp ":" ETag ":" private-key) 。time-stamp为服务器时钟,ETag为请求的Etag头。private-key为服务器知道的一个值。
opaque:服务器产生的由客户下去请求时原样返回。最好是Base64串或十六进制字符串。
auth-param:为扩展用的,现阶段忽略。
其他域请参考RFC2617。
授权头语法:
       credentials      = "Digest" digest-response
       digest-response  = 1#( username | realm | nonce | digest-uri
                       | response | [ algorithm ] | [cnonce] |
                       [opaque] | [message-qop] |
                           [nonce-count]  | [auth-param] )
       username         = "username" "=" username-value
       username-value   = quoted-string
       digest-uri       = "uri" "=" digest-uri-value
       digest-uri-value = request-uri   ; As specified by HTTP/1.1
       message-qop      = "qop" "=" qop-value
       cnonce           = "cnonce" "=" cnonce-value
       cnonce-value     = nonce-value
       nonce-count      = "nc" "=" nc-value
       nc-value         = 8LHEX
       response         = "response" "=" request-digest
       request-digest =  32LHEX 
       LHEX             =  "0" | "1" | "2" | "3" |
                           "4" | "5" | "6" | "7" |
                           "8" | "9" | "a" | "b" |
                           "c" | "d" | "e" | "f"
response:加密后的密码
digest-uri:拷贝Request-Line,用于Proxy
cnonce:如果qop设置,才设置,用于双向认证,防止攻击。
nonce-count:如果服务器看到同样的计数,就是一次重放。
示例:
401响应:        HTTP/1.1 401 Unauthorized
         WWW-Authenticate: Digest
                 realm="testrealm@host.com",
                 qop="auth,auth-int",
                 nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
                 opaque="5ccc069c403ebaf9f0171e9517f40e41"
再次请求:
         Authorization: Digest username="Mufasa",
                 realm="testrealm@host.com",
                 nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
                 uri="/dir/index.html",
                 qop=auth,
                 nc=00000001,
                 cnonce="0a4f113b",
                 response="6629fae49393a05397450978507c4ef1",
                 opaque="5ccc069c403ebaf9f0171e9517f40e41"
4.比较基本认证和摘要访问认证都是很脆弱的。基本认证可以让窃听者直接获得用户名和密码,而摘要访问认证窃听者只能获得一次请求的文档。

上一篇:【死磕Java并发】-----Java内存模型之happens-before


下一篇:使用Oracle SQL Developer报错:Unable to find a Java Virtual Machine