摘要认证( 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所示:
摘要响应中的各个参数意义如下:
nonce:客户端使用这个“现时”来产生摘要响应(digest response),需要和server 盘问中携带的nonce保持一致,这样服务器也会在一个摘要响应中收到“现时”的内容。服务器先要检查了“现时”的有效性后,才会检查摘要响应的其它部分。
因而,nonce 在本质上是一种标识符,确保收到的摘要机密,是从某个特定的摘要盘问产生的。还限制了摘要盘问的生命期,防止未来的重播攻击。
qop:客户端选择的保护方式。
nc (现时计数器):这是一个16进制的数值,即客户端发送出请求的数量(包括当前这个请求),这些请求都使用了当前请求中这个“现时”值。例如,对一个给定的“现时”值,在响应的第一个请求中,客户端将发送“nc=00000001”。这个指示值的目的,是让服务器保持这个计数器的一个副本,以便检测重复的请求。如果这个相同的值看到了两次,则这个请求是重复的。
response:这是由用户代理软件计算出的一个字符串,以证明用户知道口令。比如可以通过 username、password、http method、uri、以及nonce、qop等使用MD5加密生成。
cnonce:这也是一个不透明的字符串值,由客户端提供,并且客户端和服务器都会使用,以避免用明文文本。这使得双方都可以查验对方的身份,并对消息的完整性提供一些保护。
uri:这个参数包含了客户端想要访问的URI。
// 解析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'];
}
?>
UserLoginTask mAuthTask = new UserLoginTask(email, password);
mAuthTask.execute((Void) null);
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);
}
}
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> headers = httpResponse.getHeaders();
List wwwAuthenticate = headers.get("WWW-Authenticate");
String auth = wwwAuthenticate.get(0);
if (auth == null) {
}
// Digest
if (auth.startsWith(DIGEST.trim())) {
//这里实现Digest Auth逻辑
HashMap 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 splitAuthFields(String authString) {
final HashMap 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/