什么是摘要认证
摘要认证( 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。
- 客户端反馈用户身份
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。
- server 确认用户
确认用户主要由两部分构成:
- server 确认用户
- 检查nonce的有效性
- 检查摘要响应中的其他信息, 比如server可以按照和客户端同样的算法生成一个response值,和client传递的response进行对比。
代码实现
Server 端
<?php// 解析PHP_AUTH_DIGESTfunction 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;}@Overrideprotected 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;}@Overrideprotected void onPostExecute(final Boolean success) {mAuthTask = null;showProgress(false);if (success) {finish();} else {mPasswordView.setError(getString(R.string.error_incorrect_password));mPasswordView.requestFocus();}}@Overrideprotected void onCancelled() {mAuthTask = null;showProgress(false);}@Overridepublic void onSuccess(String result) {Log.i(TAG,"result:" + result);}@Overridepublic 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 serviceprivate 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-froyoif (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) {}// Digestif (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/



















