<导航

HTTP摘要认证原理以及HttpClient4.3实现

基本认证便捷灵活,但极不安全。用户名和密码都是以明文形式传送的,也没有采取任何措施防止对报文的篡改。安全使用基本认证的唯一方式就是将其与 SSL 配合使用。

摘要认证是另一种HTTP认证协议,它试图修复Basci认证的严重缺陷,即进行如下改进:

1, 通过传递用户名,密码等计算出来的摘要来解决明文方式在网络上发送密码的问题。

2, 通过服务产生随机数nonce的方式可以防止恶意用户捕获并重放认证的握手过程。

3, 通过客户端产生随机数cnonce的方式,支持客户端对服务器的认证。

4, 通过对内容也加入摘要计算的方式,可以有选择的防止对报文内容的篡改。

摘要认证并不是最安全的协议。摘要认证并不能满足安全 HTTP 事务的很多需求。对这些需求来说,使用 TLS 和 HTTPS 协议更为合适一些。但摘要认证比它要取代的基本认证强大很多。

一,用摘要保护密码

摘要认证的一个改进之处是用摘要代替密码的传输,遵循的基本原则是“绝对不通过网络发送明文密码”,而是发送一个密码的摘要信息,并且这摘要信息是不可逆的,即无法通 过摘要信息反推出密码信息。而服务器本身是存储这个密码的(实际上,服务器只需知道密码的摘要即可),而客户端和服务器本身都知道这个密码。这样的话,服务器可以读取客户端的摘要和本身知道的密码进行同样计算得出的摘要进行比较,若匹配,则验证通过。

摘要是对信息主体的浓缩,摘要是一种单向函数,主要用于将无限的输入值转为有限的浓缩输出值,如MD5,则是将任意长度的字节系列转换为一个128位的摘要。MD5输出的128位的摘要通常会写出32个十六进制的字符,每个字符表示4个bit。

 

二,用随机数防止重放攻击

使用单向摘要就无需以明文形式发送密码了,可以只发送密码的摘要,并且可以确信,没有哪个恶意用户能轻易的从摘要中解码出原始密码。

但是,摘要被截获也可能跟密码一起好用,为了防止重放攻击的发送,服务器可以向客户端发送一个称为随机数nonce的特殊令牌,这个数会经常发生变化(可能是每毫秒,或者每次认证都发生变化,具体由服务器控制),客户端在计算摘要之前要先将这个随机数附加到密码上去。这样,在密码中加入随机数就会使得摘要随着随机数的每次变化而变化,记录下的密码摘要只对特定的随机数有效,而没有密码的话,攻击者就无法计算出正确的摘要,这样就可以防止重放攻击的发生。

摘要认证要求使用随机数,随机数是在WWW-Authenticate服务器质询响应中从服务器传输给客户端的。

 

三,摘要认证的握手过程

1, 第一次客户端请求的时候,服务器产生一个随机数nonce,服务器将这个随机数放在WWW-Authenticate响应头,与服务器支持的认证算法列表,认证的域realm一起发送给客户端,如下例子:

HTTP /1.1 401 Unauthorized

WWW-Authenticate:Digest

realm= ”test realm”

qop=auth,auth-int”

nonce=”66C4EF58DA7CB956BD04233FBB64E0A4”

 

2, 客户端发现是401响应,表示需要进行认证,则弹出让用户输入用户名和密码的认证窗口,客户端选择一个算法,计算出密码和其他数据的摘要,将摘要放到Authorization的请求头中发送给服务器,如果客户端要对服务器也进行认证,这个时候,可以发送客户端随机数cnonce。如下例子:

GET/cgi-bin/checkout?a=b HTTP/1.1

Authorization: Digest

username=”tenfyguo”

realm=”test realm”

nonce=” 66C4EF58DA7CB956BD04233FBB64E0A4” //服务器端的随机数一起带回

uri=”/cgi-bin/checkout?a=b” //必须跟请求行一致

qop=”auth” //保护质量参数

nc=0000001

cnonce=”xxxxx234132543strwerr65sgdrftdfytryts” //客户端随机数,用于对称校验

response=” ABC4EF58DA7CB956BD04345FBB64E0A4”//最终摘要

 

3, 服务接受摘要,选择算法以及掌握的数据,重新计算新的摘要跟客户端传输的摘要进行比较,验证是否匹配,若客户端反过来用客户端随机数对服务器进行质询,就会创建客户端摘要,服务可以预先将下一个随机数计算出来,提前传递给客户端,通过Authentication-Info发送下一个随机数。如下例子:

HTTP/1.1 200 OK

Authorization-Info:nextnonce=” 88C4EF58DA7CB956BD04233FBB64E0A4”

qop=”auth”

rspauth=”23543534DfasetwerwgDTerGDTERERRE”

cnonce=” xxxxx234132543strwerr65sgdrftdfytryts”

 

 

 

四,摘要的计算

在说明如何计算摘要之前,先说明参加摘要计算的信息块。信息块主要有两种:

1,表示与安全相关的数据的A1。

A1中的数据时密码和受保护信息的产物,它包括用户名,密码,保护域和随机数等内容,A1只涉及安全信息,与底层报文自身无关。

若算法是:MD5

则A1=<user>:<realm>:<password>

若算法是:MD5-sess

则A1=MD5(<user>:<realm>:<password>):<nonce>:<cnonce>

 

2,表示与报文相关的数据的A2.

A2表示是与报文自身相关的信息,比如URL,请求反复和报文实体的主体部分,A2加入摘要计算主要目的是有助于防止反复,资源或者报文被篡改。

若qop未定义或者auth:

A2=<request-method>:<uri-directive-value>

若qop为auth:-int

A2=<request-method>:<uri-directive-value>:MD5(<request-entity-body>)

 

下面定义摘要的计算规则:

若qop没有定义:

摘要response=MD5(MD5(A1):<nonce>:MD5(A2))

 

若qop为auth:

摘要response=MD5(MD5(A1):<nonce>:<nc>:<cnonce>:<qop>:MD5(A2))

 

若qop为auth-int:

摘要response= MD5(MD5(A1):<nonce>:<nc>:<cnonce>:<qop>:MD5(A2))

 

五,随机数的生成

RFC2617建议采用这个假想的随机数公式:

nonce = BASE64(time-stamp MD5(time-stamp “:” ETag “:” private-key))

其中:

time-stamp是服务器产生的时间戳或者其他不会重复的序列号,ETag是与所请求实体有关的HTTP ETag首部的值,priviate-key是只有服务器知道的数据。

 

这样,服务器就可以收到客户端的认证首部之后重新计算散列部分,如果结果与那个首部的随机数不符,或者是时间戳的值不够新,就可以拒绝请求,服务器可以通过这种方式来限制随机数的有效持续时间。

 

包括了ETag可以防止对已经更新资源版本的重放请求。注意:在随机数中包含客户端IP,服务器好像就可以限制原来获取此随机数的客户端重用这个随机数了,但这会破坏代理集群的工作,使用代理集群时候,来自单个用户的多条请求通常会经过不同的代理进行传输,而且IP地址欺骗实现起来也不复杂。

六,摘要认证的工作原理

下面来看看摘要认证的工作原理(简化版本):

a) 客户端请求了某个受保护文档。

b) 在客户端能够证明其知道密码从而确认其身份之前,服务器拒绝提供文档。服务器向客户端发起质询,询问用户名和摘要形式的密码。

c) 客户端传递了密码的摘要,证明它是知道密码的。服务器知道所有用户的秘密,因此可以将客户提供的摘要与服务器自己计算得到的摘要进行比较,以验证用户是否知道密码。另一方在不知道密码的情况下,很难伪造出正确的摘要。

d) 服务器将客户端提供的摘要与服务器内部计算出的摘要进行对比。如果匹配,就说明客户端知道密码(或者很幸运地猜中了!)。可以设置摘要函数,使其产生很多数字,让人不可能幸运地猜中摘要。服务器进行了匹配验证之后,会将文档提供给客户端——整个过程都没有在网络上发送密码。

 七,重放攻击

使用摘要就无需以明文形式发送密码了。可以只发送密码的摘要,而且可以确信,没有哪个恶意用户能轻易地从摘要中解码出原始密码。

但是,仅仅隐藏密码并不能避免危险,因为即便不知道密码,别有用心的人也可以截获摘要,并一遍遍地重放给服务器。摘要和密码一样好用。

为防止此类重放攻击的发生,服务器可以向客户端发送了一个称为随机数 (nonce) 的特殊令牌,这个数会经常发生变化(可能是每毫秒,或者是每次认证都变化)。客户端在计算摘要之前要先将这个随机数令牌附加到密码上去。

在密码中加入随机数就会使摘要随着随机数的每一次变化而变化。记录下的密码摘要只对特定的随机数有效,而没有密码的话,攻击者就无法计算出正确的摘要,这样就可以防止重放攻击的发生。

摘要认证要求使用随机数,因为这个小小的重放弱点会使未随机化的摘要认证变得和基本认证一样脆弱。随机数是在 WWW-Authenticate 质询中从服务器传送给客户端。

 八,摘要认证的握手机制

下面是简化的摘要认证三步握手机制:

(1) 服务器计算出一个随机数。

(2) 服务器将这个随机数放在 WWW-Authenticate 质询报文中,与服务器所支持的算法列表一同发往客户端。

(3) 客户端选择一个算法,计算出密码和其他数据的摘要。

(4) 客户端将摘要放在一条 Authorization 报文中发回服务器。如果客户端要对服务器进行认证,可以发送客户端随机数。

(5) 服务器接收摘要、选中的算法以及支撑数据,计算出与客户端相同的摘要。然后服务器将本地生成的摘要与网络传送过来的摘要进行比较,验证是否匹配。如果客户端反过来用客户端随机数对服务器进行质询,就会创建客户端摘要。服务器可以预先将下一个随机数计算出来,提前将其传递给客户端,这样下一次客户端就可以预先发送正确的摘要了。

 九,预授权

普通的认证方式中,事务结束之前,每条请求都要有一次请求/质询的需要,参见下图 (a)。

如果客户端事先知道下一个随机数是什么,就可以取消这个请求/质询循环,这样客户端就可以在服务器发出请求之前,生成正确的 Authorization 首部了。如果客户端能在服务器要求他计算 Authorization 首部之前将其计算出来,就可以预先将 Authorization 首部发送给服务器,而不用进行请求/质询了。下图 (b) 显示了这种方式对性能的影响。

预授权对基本认证来说并不重要(而且很常见)。浏览器通常会维护一些客户端数据库以存储用户名和密码。一旦用户与某站点进行了认证,浏览器通常会为后继对那个 URL 的请求发送正确的 Authorization 首部。

由于摘要认证使用了随机数技术来破坏重放攻击,所以对摘要认证来说,预授权要稍微复杂一些。服务器会产生任意的随机数,所以在客户端收到质询之前,不一定总能判定应该发送什么样的 Authorization 首部。

摘要认证在保留了很多安全特性的同时,还提供了集中预授权方式。这里列出了三种可选的方式,通过这些方式,客户端无效等待新的 WWW-Authenticate 质询,就可以获得正确的随机数:

  • 服务器预先在 Authentication-Info 成功首部中发送下一个随机数;
  • 服务器允许在一小段时间内使用同一个随机数;
  • 客户端和服务器使用同步的、可预测的随机数算法。

预先生成下一个随机数

可以在 Authentication-Info 成功首部中将下一个随机数预先提供给客户端。这个首部是与前一次成功认证的 200 OK 响应一同发送的。

Authentication-Info: nextnonce="<nonce-value>"

有了下一个随机数,客户端就可以预先发布 Authorization 首部了。

尽管这种预授权机制避免了请求/质询循环(加快了事务处理的速度),但实际上它也破坏了对同一台服务器的多条请求进行管道化的功能,因为在发布下一条请求之前,一定要收到下一个随机值才行。而管道化是避免延迟的一项基本技术。所以这样可能会造成很大的性能损失。

受限制的随机数重用机制

另一种方法不是预先生成随机数序列,而是在有限的次数内重用随机数。比如,服务器可能允许将某个随机重用 5 次,或者重用 10 秒。

在这种情况下,客户端可以随意发布带有 Authorization 首部的请求,而且由于随机数是事先知道的,所以还可以对请求进行管道化。随机数过期时,服务器要向客户端发送 401 Unauthorized 质询,并设置 WWW-Authenticate:stale=true 指令:

WWW-Authenticate: Digest realm="<realm-value>", nonce="<nonce-value>", stale=true

重用随机数使得攻击者更容易成功地实行重放攻击。虽然这确实降低了安全性,但重用的随机数的生存周期是可控的(从严格禁止重用到较长时间的重用),所以应该可以在安全和性能间找到平衡。

同步生成随机数

还可以采用时间同步的随机数生成算法,客户端和服务器可根据共享的密钥,生成第三方无法轻易预测的、相同的随机数序列。

 

文章最后附上HttpClient4.2.3以及HttpClient4.3摘要认证亲测实例:

 

public static void httpSend(String url,String userName,String passWord,String xmlParam){
        CloseableHttpClient httpClient = null;
        try {
            URI serverURI = new URI(url);
            //摘要认证处理
            /*已过时的:DefaultHttpClient httpClient = new DefaultHttpClient();
            Credentials creds = new UsernamePasswordCredentials(userName,passWord);
            httpClient.getCredentialsProvider().setCredentials(new AuthScope(serverURI.getHost(), serverURI.getPort()), (Credentials) creds);
            httpClient.getParams().setParameter(AuthSchemes.DIGEST, Collections.singleton(AuthSchemes.DIGEST));
            httpClient.getAuthSchemes().register(AuthSchemes.DIGEST,new DigestSchemeFactory());*/
            //设置超时代码
            /*RequestConfig requestConfig = RequestConfig.custom()  
                    .setConnectTimeout(5000).setConnectionRequestTimeout(3000)  
                    .setSocketTimeout(3000).build();  
            httpClient = HttpClients.custom().setDefaultRequestConfig(requestConfig).build();*/
            
            CredentialsProvider credsProvider = new BasicCredentialsProvider();
            credsProvider.setCredentials(new AuthScope(serverURI.getHost(), serverURI.getPort()),
                    new UsernamePasswordCredentials(userName,passWord));
            httpClient = HttpClients.custom().setDefaultCredentialsProvider(credsProvider).build();
            
            HttpPost post = new HttpPost(url);
              // 构造消息头
              /*post.setHeader("Content-type", "application/json; charset=utf-8");*/

             // post.setEntity(new ByteArrayEntity(bytes));//发送二进制数组数据如:图片的二进制数据  

            post.setEntity(new StringEntity(xmlParam,"UTF-8"));//发送xml字符串数据
            HttpResponse response = httpClient.execute(post);
            String result = EntityUtils.toString(response.getEntity());
            System.out.println("返回的消息:" + result);
            System.out.println("返回的状态" + response.getStatusLine().getStatusCode());
            System.out.println(response.getEntity().getContentType());
            EntityUtils.consume(response.getEntity());
            httpClient.close();
        } catch (Exception e) {
            e.printStackTrace();
        }finally{
            if(httpClient != null){
                try {
                    httpClient.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        } 
    }
    private static String getXmlString() {
         StringBuilder sb=new StringBuilder();
            try {   
             InputStream inputStream = DigestDemo.class.getResourceAsStream("searchFc2.xml");
                BufferedReader br=new BufferedReader(new InputStreamReader(inputStream));   
                String line="";   
                for(line=br.readLine();line!=null;line=br.readLine()) {   
                    sb.append(line+"\n");   
                }   
            } catch (FileNotFoundException e) {   
                e.printStackTrace();
            } catch (IOException e) {   
                e.printStackTrace();
            }
            return sb.toString();   
     }
    
    public static void main(String[] args) throws Exception {
        String url = "http://";
        String userName = "";
        String passWord = "";
        String xmlParam = getXmlString();
        httpSend(url,userName,passWord,xmlParam);
        
    }
    //测试接收方部分代码
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
     ...
        request.setCharacterEncoding("utf-8");
        BufferedReader reader = null;
        String result = "";
        StringBuffer sbf = new StringBuffer();
        InputStream is = request.getInputStream();
        reader = new BufferedReader(new InputStreamReader(is, "utf-8"));
        String strRead = null;
        while ((strRead = reader.readLine()) != null) {
            sbf.append(strRead);
            sbf.append("\r\n");
        }
        reader.close();
        result = sbf.toString();
        System.out.println("result:"+result);
        ...
    }

 

posted @ 2018-05-24 16:00  字节悦动  阅读(2916)  评论(0编辑  收藏  举报