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=./\_-]+)\2@', $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'];
    }
?>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69

client 端 (Android)

  • LoginActivity.java
 UserLoginTask mAuthTask = new UserLoginTask(email, password);
 mAuthTask.execute((Void) null);
  • 1
  • 2
  • 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);
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 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);
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232

HTTP Digest Auth的学习过程实在奇怪,首先google/baidu了一大堆文章,但是前方出现大量的艰深难懂的术语,轻易的让我brain fart。无奈先参考PHP官方文档coding了吧。coding一遍之后,那些词汇的意义仿佛清晰了很多。感谢BabyUnion的总结:http://blog.163.com/hlz_2599/blog/static/1423784742013415101252410/

猜你喜欢

转载自blog.csdn.net/gnnulzy/article/details/79822354
今日推荐