1. 程式人生 > >HTTP Digest authentication

HTTP Digest authentication

什麼是摘要認證

摘要認證( Digest authentication)是一個簡單的認證機制,最初是為HTTP協議開發的,因而也常叫做HTTP摘要,在RFC2617中描述。其身份驗證機制很簡單,它採用雜湊式(hash)加密方法,以避免用明文傳輸使用者的口令。

摘要認證就是要核實,參與通訊的雙方,都知道雙方共享的一個祕密(即口令)。

摘要認證流程

圖一

  • 伺服器核實使用者身份

    server收到client的HTTP request(INVITE),如果server需要客戶端摘要認證,就需要生成一個摘要盤問(digest challenge),通過Response給client一個401 Unauthorized狀態傳送給使用者。

    摘要盤問如 圖二 中的WWW-Authenticate header所示:

    圖二

    摘要盤問中的各個引數意義如下:

  • realm(領域):必須的,在所有的盤問中都必須有。它是目的是鑑別SIP訊息中的機密。在實際應用中,它通常設定為server所負責的域名。

  • nonce (現時):必須的,這是由伺服器規定的資料字串,在伺服器每次產生一個摘要盤問時,這個引數都是不一樣的(與前面所產生的不會雷同)。nonce 通常是由一些資料通過md5雜湊運算構造的。這樣的資料通常包括時間標識和伺服器的機密短語。確保每個nonce 都有一個有限的生命期(也就是過了一些時間後會失效,並且以後再也不會使用),而且是獨一無二的

    (即任何其它的伺服器都不能產生一個相同的nonce )。

  • Stale:不必須,一個標誌,用來指示客戶端先前的請求因其nonce值過期而被拒絕。如果stale是TRUE(大小寫敏感),客戶端可能希望用新的加密迴應重新進行請求,而不用麻煩使用者提供新的使用者名稱和口令。伺服器端只有在收到的請求nonce值不合法,而該nonce對應的摘要(digest)是合法的情況下(即客戶端知道正確的使用者名稱/口令),才能將stale置成TRUE值。如果stale是FALSE或其它非TRUE值,或者其stale域不存在,說明使用者名稱、口令非法,要求輸入新的值。

  • opaque(不透明體):必須的,這是一個不透明的(不讓外人知道其意義)資料字串,在盤問中傳送給使用者。

  • algorithm(演算法):不必須,這是用來計算雜湊的演算法。當前只支援MD5演算法。

  • qop(保護的質量):必須的,這個引數規定伺服器支援哪種保護方案。客戶端可以從列表中選擇一個。值 “auth”表示只進行身份查驗, “auth-int”表示進行查驗外,還有一些完整性保護。需要看更詳細的描述,請參閱RFC2617

    1. 客戶端反饋使用者身份

    client 生成 生成摘要響應(digest response),然後再次通過 http request (INVITE (Withink digest))發給 server。

    摘要響應如 圖三 中的Authenticate header所示:

    圖三

    摘要響應中的各個引數意義如下:

  • username: 不用再說明了
  • realm: 需要和 server 盤問的realm保持一致
  • nonce:客戶端使用這個“現時”來產生摘要響應(digest response),需要和server 盤問中攜帶的nonce保持一致,這樣伺服器也會在一個摘要響應中收到“現時”的內容。伺服器先要檢查了“現時”的有效性後,才會檢查摘要響應的其它部分。

    因而,nonce 在本質上是一種識別符號,確保收到的摘要機密,是從某個特定的摘要盤問產生的。還限制了摘要盤問的生命期,防止未來的重播攻擊。

  • qop:客戶端選擇的保護方式。

  • nc (現時計數器):這是一個16進位制的數值,即客戶端傳送出請求的數量(包括當前這個請求),這些請求都使用了當前請求中這個“現時”值。例如,對一個給定的“現時”值,在響應的第一個請求中,客戶端將傳送“nc=00000001”。這個指示值的目的,是讓伺服器保持這個計數器的一個副本,以便檢測重複的請求。如果這個相同的值看到了兩次,則這個請求是重複的。

  • response:這是由使用者代理軟體計算出的一個字串,以證明使用者知道口令。比如可以通過 username、password、http method、uri、以及nonce、qop等使用MD5加密生成。

  • cnonce:這也是一個不透明的字串值,由客戶端提供,並且客戶端和伺服器都會使用,以避免用明文文字。這使得雙方都可以查驗對方的身份,並對訊息的完整性提供一些保護。

  • uri:這個引數包含了客戶端想要訪問的URI。

    1. server 確認使用者
      確認使用者主要由兩部分構成:
  • 檢查nonce的有效性
  • 檢查摘要響應中的其他資訊, 比如server可以按照和客戶端同樣的演算法生成一個response值,和client傳遞的response進行對比。

程式碼實現

Server 端

<?php

   // 解析PHP_AUTH_DIGEST
   function http_digest_parse($txt)
   {
       // 判斷 Authorization資料是否完整
       $needed_parts = array(
           'nonce' => 1,
           'nc' => 1,
           'cnonce' => 1,
           'qop' => 1,
           'username' => 1,
           'uri' => 1,
           'response' => 1
       );
       $data = array();

       //把 txt 解析成了二維陣列,結構 array(array('key','"','value'),...);
       preg_match_all('@(\w+)=([\'"]?)([a-zA-Z0-9=./\_-]+)\[email protected]', $txt, $matches, PREG_SET_ORDER);

       foreach ($matches as $m) {
           //$m[1]是key值,$m[3]是value值
           $data[$m[1]] = $m[3];
           //將needed_parts中對應的key-value釋放掉
           unset($needed_parts[$m[1]]);
       }

       //判斷needed_parts是否已經被完全釋放,如果是,則Authorization資料完整且解析成功,否則,解析失敗
       return $needed_parts ? false : $data;
   }

    public function digest_authorization()
    {
        $realm = 'Restricted area';
        // username => password
        $users = array(
            'admin' => 'mypass',
            'guest' => 'guest'
        );

        //響應客戶端 INVITE 的請求
        if (empty($_SERVER['PHP_AUTH_DIGEST'])) {
            header('HTTP/1.1 401 Unauthorized');
            header('WWW-Authenticate: Digest realm="' . $realm . '",qop="auth",nonce="' . uniqid() . '",opaque="' . md5($realm) . '"');
            die('Text to send if user hits Cancel button');
        }

        // 分析Authorization header資料,如果Authorization header資料未被成功解析,或者不能根據Authorization header中的username查詢密碼,響應憑證錯誤
        if (! ($data = http_digest_parse($_SERVER['PHP_AUTH_DIGEST'])) || ! isset($users[$data['username']])) {
            die('Wrong Credentials!');
        }

        // 生成 有效的response
        // A1 = md5(Authorization header中的username + 本地realm + 根據Authorization header中的username查詢的密碼);
        $A1 = md5($data['username'] . ':' . $realm . ':' . $users[$data['username']]);
        // A2 同 客戶端生成的方式
        $A2 = md5($_SERVER['REQUEST_METHOD'] . ':' . $data['uri']);
        // valid_code 同 客戶端生成的方式
        $valid_code = md5($A1 . ':' . $data['nonce'] . ':' . $data['nc'] . ':' . $data['cnonce'] . ':' . $data['qop'] . ':' . $A2);

        //對比請求中攜帶的response和伺服器生成的valid_code,如果不一致則響應憑據錯誤
        if ($data['response'] != $valid_code) {
            die('Wrong Credentials!');
        }

        // 校驗通過,則根據uri獲取並返回資料 
        echo 'Your are logged in as: ' . $data['username'];
    }
?>

client 端 (Android)

  • LoginActivity.java
 UserLoginTask mAuthTask = new UserLoginTask(email, password);
 mAuthTask.execute((Void) null);
  • UserLoginTask.java
public class UserLoginTask extends AsyncTask<Void, Void, Boolean> implements HttpHelper.Callback{

        private final String mEmail;
        private final String mPassword;

        UserLoginTask(String email, String password) {
            mEmail = email;
            mPassword = password;
        }

        @Override
        protected Boolean doInBackground(Void... params) {
            // TODO: attempt authentication against a network service.
            try {
                HttpHelper httpHelper = new HttpHelper(URL,mEmail,mPassword,getApplicationContext());
                httpHelper.setCallback(this);
                httpHelper.fetchHttpData();

            } catch (IOException e) {
                e.printStackTrace();
            }
            for (String credential : DUMMY_CREDENTIALS) {
                String[] pieces = credential.split(":");
                if (pieces[0].equals(mEmail)) {
                    // Account exists, return true if the password matches.
                    return pieces[1].equals(mPassword);
                }
            }

            // TODO: register the new account here.
            return true;
        }

        @Override
        protected void onPostExecute(final Boolean success) {
            mAuthTask = null;
            showProgress(false);

            if (success) {
                finish();
            } else {
                mPasswordView.setError(getString(R.string.error_incorrect_password));
                mPasswordView.requestFocus();
            }
        }

        @Override
        protected void onCancelled() {
            mAuthTask = null;
            showProgress(false);
        }

        @Override
        public void onSuccess(String result) {
                Log.i(TAG,"result:" + result);
        }

        @Override
        public void onUnauthorized(String result) {
            Log.i(TAG,"result:" + result);
        }
    }
  • HttpHelper.java

import android.content.Context;
import android.os.Build;
import android.text.TextUtils;
import android.util.Log;

import com.google.common.base.CharMatcher;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.turbomanage.httpclient.BasicHttpClient;
import com.turbomanage.httpclient.ConsoleRequestLogger;
import com.turbomanage.httpclient.HttpMethod;
import com.turbomanage.httpclient.HttpResponse;
import com.turbomanage.httpclient.RequestLogger;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class HttpHelper {
    private static final String TAG = "HttpHelper";

    private static final String BASIC = "Basic ";
    private static final String DIGEST = "Digest ";

    private static final String NONCE = "nonce";
    private static final String QOP = "qop";
    private static final String REALM = "realm";
    private static final String OPAQUE = "opaque";

    private static final String USERNAME = "username";
    private static final String NC = "nc";
    private static final String nc = "00000001";
    private static final String CNONCE = "cnonce";
    private static final String cnonce = "0a4f113b";
    private static final String RESPONSE = "response";
    private static final String URI = "uri";

    // URL of the remote service
    private String mUri = null;

    private String mRemoteService = null;

    private String mTimestamp = null;

    private String mUsername = null;

    private String mPassword = null;

    private Context mContext = null;

    Callback mCallback = null;

    static {
        // HTTP connection reuse which was buggy pre-froyo
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO) {
            System.setProperty("http.keepAlive", "false");
        } else {
            System.setProperty("http.keepAlive", "true");
        }
    }

        private RequestLogger mQuietLogger = new ConsoleRequestLogger();

    public HttpHelper(String uri, Context context) {
        mUri = uri;
        mContext = context;
        mRemoteService = "Server IP" + mUri;
    }

    public HttpHelper(String url, String timestamp, Context context) {
        this(url, context);
        mTimestamp = timestamp;
    }

    public HttpHelper(String url, String username, String password, Context context) {
        this(url, context);
        mUsername = username;
        mPassword = password;
    }

    public void setCallback(Callback callback) {
        mCallback = callback;
    }

    private void fetchHttpResponse(HttpResponse httpResponse) throws IOException {
        int status = httpResponse.getStatus();
        if (status == HttpURLConnection.HTTP_OK) {
            String str = httpResponse.getBodyAsString();
            mCallback.onSuccess(str);
        } else if (status == HttpURLConnection.HTTP_UNAUTHORIZED) {
            //伺服器獲得401響應,並解析WWW-Authenticate header的型別
            String str = httpResponse.getBodyAsString();
            mCallback.onUnauthorized(str);
            Map<String, List<String>> headers = httpResponse.getHeaders();
            List<String> wwwAuthenticate = headers.get("WWW-Authenticate");
            String auth = wwwAuthenticate.get(0);
            if (auth == null) {
            }
            // Digest
            if (auth.startsWith(DIGEST.trim())) {
                //這裡實現Digest Auth邏輯
                HashMap<String, String> authFields = splitAuthFields(auth.substring(7));
                Joiner colonJoiner = Joiner.on(':');
                String A1 = null; //A1 = MD5("usarname:realm:password");
                String A2 = null; //A2 = MD5("httpmethod:uri");
                String response = null; //response = MD5("A1:nonce:nc:cnonce:qop:A2");

                MessageDigest md5 = null;
                try {
                    md5 = MessageDigest.getInstance("MD5");
                } catch (NoSuchAlgorithmException e) {
                    e.printStackTrace();
                }
                try {
                    md5.reset();
                    String A1Str = colonJoiner.join(mUsername, authFields.get(REALM), mPassword);
                    md5.update(A1Str.getBytes("ISO-8859-1"));
                    A1 = bytesToHexString(md5.digest());
                } catch (UnsupportedEncodingException e) {
                }
                try {
                    md5.reset();
                    String A2Str = colonJoiner.join(HttpMethod.GET.toString(), mUri);
                    md5.update(A2Str.getBytes("ISO-8859-1"));
                    A2 = bytesToHexString(md5.digest());
                } catch (UnsupportedEncodingException e) {
                }
                try {
                    md5.reset();
                    String A2Str = colonJoiner.join(A1, authFields.get(NONCE), nc, cnonce, authFields.get(QOP), A2);
                    md5.update(A2Str.getBytes("ISO-8859-1"));
                    response = bytesToHexString(md5.digest());
                } catch (UnsupportedEncodingException e) {
                }
                // 拼接 Authorization Header,格式如 Digest username="admin",realm="Restricted area",nonce="554a3304805fe",qop=auth,opaque="cdce8a5c95a1427d74df7acbf41c9ce0", nc=00000001,response="391bee80324349ea1be02552608c0b10",cnonce="0a4f113b",uri="/MyBlog/home/Response/response_last_modified"
                StringBuilder sb = new StringBuilder();
                sb.append(DIGEST);
                sb.append(USERNAME).append("=\"").append(mUsername).append("\",");
                sb.append(REALM).append("=\"").append(authFields.get(REALM)).append("\",");
                sb.append(NONCE).append("=\"").append(authFields.get(NONCE)).append("\",");
                sb.append(QOP).append("=").append(authFields.get(QOP)).append(",");
                sb.append(OPAQUE).append("=\"").append(authFields.get(OPAQUE)).append("\",");
                sb.append(NC).append("=").append(nc).append(",");
                sb.append(RESPONSE).append("=\"").append(response).append("\",");
                sb.append(CNONCE).append("=\"").append(cnonce).append("\",");
                sb.append(URI).append("=\"").append(mUri).append("\"");
                //再請求一次
                BasicHttpClient httpClient = new BasicHttpClient();
                httpClient.setRequestLogger(mQuietLogger);

                if (mTimestamp != null && !TextUtils.isEmpty(mTimestamp)) {
                    if (TimeUtils.isValidFormatForIfModifiedSinceHeader(mTimestamp)) {
                        httpClient.addHeader("If-Modified-Since", mTimestamp);
                    } else {
                        Log.w(TAG, "Could not set If-Modified-Since HTTP header. Potentially downloading " +
                                "unnecessary data. Invalid format of refTimestamp argument: " + mTimestamp);
                    }
                }
                httpClient.addHeader("Authorization", sb.toString());
                fetchHttpResponse(httpClient.get(mRemoteService, null));
            } else if (auth.startsWith(BASIC.trim())) { // Basic
                //這裡實現Basic Auth邏輯
            }
        } else if (status == HttpURLConnection.HTTP_NOT_MODIFIED) {
            Log.d(TAG, "HTTP_NOT_MODIFIED: data has not changed since " + mTimestamp);
        } else {
            Log.e(TAG, "Error fetching conference data: HTTP status " + status);
            throw new IOException("Error fetching conference data: HTTP status " + status);
        }
    }

    public void fetchHttpData() throws IOException {
        if (TextUtils.isEmpty(mUri)) {
            Log.w(TAG, "Manifest URL is empty.");
        }
        BasicHttpClient httpClient = new BasicHttpClient();
        httpClient.setRequestLogger(mQuietLogger);

        if (mTimestamp != null && !TextUtils.isEmpty(mTimestamp)) {
            if (TimeUtils.isValidFormatForIfModifiedSinceHeader(mTimestamp)) {
                httpClient.addHeader("If-Modified-Since", mTimestamp);
            } else {
                Log.w(TAG, "Could not set If-Modified-Since HTTP header. Potentially downloading " +
                        "unnecessary data. Invalid format of refTimestamp argument: " + mTimestamp);
            }
        }

        HttpResponse response = httpClient.get(mRemoteService, null);
        fetchHttpResponse(response);
    }

    private static HashMap<String, String> splitAuthFields(String authString) {
        final HashMap<String, String> fields = Maps.newHashMap();
        final CharMatcher trimmer = CharMatcher.anyOf("\"\t ");
        final Splitter commas = Splitter.on(',').trimResults().omitEmptyStrings();
        final Splitter equals = Splitter.on('=').trimResults(trimmer).limit(2);
        String[] valuePair;
        for (String keyPair : commas.split(authString)) {
            valuePair = Iterables.toArray(equals.split(keyPair), String.class);
            fields.put(valuePair[0], valuePair[1]);
        }
        return fields;
    }

    private static final String HEX_LOOKUP = "0123456789abcdef";

    private static String bytesToHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < bytes.length; i++) {
            sb.append(HEX_LOOKUP.charAt((bytes[i] & 0xF0) >> 4));
            sb.append(HEX_LOOKUP.charAt((bytes[i] & 0x0F) >> 0));
        }
        return sb.toString();
    }


    public static interface Callback {
        void onSuccess(String result);

        void onUnauthorized(String result);
    }

}

HTTP Digest Auth的學習過程實在奇怪,首先google/baidu了一大堆文章,但是前方出現大量的艱深難懂的術語,輕易的讓我brain fart。無奈先參考PHP官方文件coding了吧。coding一遍之後,那些詞彙的意義彷彿清晰了很多。感謝BabyUnion的總結:http://blog.163.com/hlz_2599/blog/static/1423784742013415101252410/

相關推薦

HTTP Digest authentication

什麼是摘要認證 摘要認證( Digest authentication)是一個簡單的認證機制,最初是為HTTP協議開發的,因而也常叫做HTTP摘要,在RFC2617中描述。其身份驗證機制很簡單,它採用雜湊式(hash)加密方法,以避免用明文傳輸使用者的口令。

C# POST訪問需要HTTP Digest Authentication認證資源的實現

在你訪問一個需要HTTP Digest Authentication的URL的時候,如果你沒有提供使用者名稱和密碼,伺服器就會返回401,如果你直接在瀏覽器中開啟,瀏覽器會提示你輸入使用者名稱和密碼;要在傳送請求的時候新增HTTP Digest Authent

HTTP 驗證 Tomcat中進行基本驗證 (Basic Authentication) 和摘要驗證 (Digest Authentication)

HTTP 驗證HTTP 協議提供驗證機制來保護資源。當一個請求要求取得受保護的資源時,網頁伺服器迴應一個 401 Unauthorized error 錯誤碼。這個迴應包含一個指定了驗證方法和領域的 WWW-Authenticate 頭資訊。把這個領域想像成一個儲存著使用者名稱和密碼的資料庫,它將被用來標識

[轉]asp.net權限認證:摘要認證(digest authentication)

imp 2個 端端 不一致 輸入 解析 操作 hash ostc 本文轉自:http://www.cnblogs.com/lanxiaoke/p/6357501.html 摘要認證簡單介紹 摘要認證是對基本認證的改進,即是用摘要代替賬戶密碼,從而防止明文傳輸中

newlisp HTTP Basic Authentication

sta style -m query duration article uil statistic visio HTTP Basic Authentication原來

Web驗證方式--Http Basic Authentication

分享 user omr figure org www gen 一起 host Http Basic Authentication是HTTP協議中定義的Web系統中的驗證方式。參考wiki 主要的實現機制如下: 1. 用戶通過瀏覽器匿名訪問web資源。 2. we

http digest鑒權

協議 第一個 資源 esp 強制 摘要認證 ica -i 時間 “摘要”式認證( Digest authentication)是一個簡單的認證機制,最初是為HTTP協議開發的,因而也常叫做HTTP摘要,在RFC2671中描述。其身份驗證機制很簡單,它采用雜湊式(hash)加

How to enable HTTP Basic Authentication in Spring Security using Java and XML Config

In the last article, I have shown you how to enable Spring security in Java application and today we'll talk about how to enable Basic HTTP authentication

Implement digest authentication in unsupported tools

Summary Digest authentication is a common authentication method that enables web servers to authenticate a user to his or her web bro

訪問需要HTTP Basic Authentication認證的資源的各種語言的實現

無聊想呼叫下嘀咕的api的時候,發現需要HTTP Basic Authentication,就看了下。 在你訪問一個需要HTTP Basic Authentication的URL的時候,如果你沒有提供使用者名稱和密碼,伺服器就會返回401,如果你直接在瀏覽器中開啟,瀏

ASP.NET Web API(三):安全驗證之使用摘要認證(digest authentication)

在前一篇文章中,主要討論了使用HTTP基本認證的方法,因為HTTP基本認證的方式決定了它在安全性方面存在很大的問題,所以接下來看看另一種驗證的方式:digest authentication,即摘要認證。 系列文章列表 摘要認證原理 在基本認證的方式中,主要的安全問題來自於使用者資訊的明文傳輸,而

以摘要認證(Digest Authentication)方式偽登入某攝像頭

密碼已知。 分析發現,該攝像頭Web登入採用了Digest Authentication的方式。流程如下: 下面大致看一下這部分的驗證流程: 1. 客戶端請求 /api/employees; 2. 服務端返回401未驗證的狀態,並且在返回的資訊中

http digest認證過程分析及例子

驗證過程:            步驟一、客戶端向伺服器申請資料                         ****************************Request******************************GET /auth HTTP/

C#如何呼叫axis釋出的帶HTTP Basic Authentication驗證的介面配置方式

C#呼叫HTTP BasicAuthentication驗證的介面步驟          由於專案的需要,需C#呼叫帶使用者名稱和密碼的webservice介面。         C#呼叫java的帶使用者名稱和密碼的webservice服務,在網上找了很多資料,也沒有測通

一個HTTP Basic Authentication引發的異常

這幾天在做一個功能,其實很簡單。就是呼叫幾個外部的API,返回資料後進行組裝然後成為新的介面。其中一個API是一個很奇葩的API,雖然是基於HTTP的,但既沒有基於SOAP規範,也不是Restful風格的介面。還好使用它也沒有複雜的場景。只是構造出URL,傳送一個HTTP的get請求,然後給我返回一個XML

HTTP Basic Authentication認證的資源的C#實現

要在傳送請求的時候新增HTTP Basic Authentication認證資訊到請求中,有兩種方法: 一是在請求頭中新增Authorization: Authorization: "Basic 使用者名稱和密碼的base64加密字串" 二是在url中新增使用者名稱和密碼:

HTTP Basic Authentication認證方式和AFNetworking的header的設定

第一:什麼是 HTTP Basic Authentication?        HTTP Basic Authentication 是一種用來允許Web瀏覽器或其他客戶端程式在請求時提供以使用者名稱

Go實戰--通過basic認證的http(basic authentication)

生命不止, 繼續 go go go !!! 今天就跟大家介紹一下帶有basic認證的api。 何為basic authentication In the context of a HTTP transaction, basic access aut

onvif http digest 鑑權認證報文流程分析

------------------------------------------------------------------------------------------------------------------------------------------

Http Digest認證協議

其認證的基本框架為挑戰認證的結構,如下圖所示:  1.客戶端希望取到伺服器上的某個資源,向伺服器傳送Get請求。 2.伺服器收到客戶端的請求後,發現這個資源需要認證資訊,判斷請求報文中是否帶有Authorization頭,如果沒有,返回一個401(Unauthorize