HTTP Digest authentication

2023-05-16

什么是摘要认证

摘要认证( 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'];
    }
?>

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/

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

HTTP Digest authentication 的相关文章

随机推荐

  • 学习、使用C++开发是不是过时了?

    C 43 43 在开发过程中真心很尴尬 1 拿相同薪水使用不同语言的程序员 xff0c 开发大多数相同的常见业务需求 xff0c C 43 43 总是进度较慢 xff08 不考虑时 空复杂性及效率 xff09 2 扩展性 跨平台 资源 内存
  • strcat()函数的用法

    这几天的一次程序练习中用到了strcat 函数 xff0c 但也想到了一些问题 我们都知道strcat str ptr 是将字符串ptr内容连接到字符串str后 xff0c 然后得到一个组合后的字符串str xff0c 比如 str字符串内
  • libQtCore.so.4 undefined symbol :g_main_context_push_thread_default

    开发板终端执行qt程序 qtDemo qws 报错 xff1a libQtCore so 4 undefined symbol g main context push thread default 解决方案 xff1a cd DVSDK p
  • curl时设置Expect的必要性

    curl 在项目中使用频率较高 xff0c 比如内部接口 第三方 api 图片存储服务等 xff0c 但是我们在使用 curl 时可能并没有注意到 Expect 这个请求头信息 xff0c 而 Expect 设置不正确 xff0c 会导致不
  • 奇偶校验原理

    奇校验 xff1a 求一个字节8位中 1 的个数 xff0c 添加一位校验位 xff0c 使9位中 1 的个数为奇数 xff1b 偶校验同理 奇校验就是让原有数据序列中 xff08 和要加上的一位 xff09 1的个数为奇数 如010001
  • CreateMutex函数函数用来实现进程互斥

    CreateMutex函数 正常情况下 xff0c 一个进程的运行一般是不会影响到其他正在运行的进程的 但是对于某些有特殊要求的如以独占方式使用串行口等硬件设备的程序就要求在其进程运行期间不允许其他试图使用此端口设备的程序运行的 xff0c
  • C++与QML交互总结

    一直对于QT的理解和使用都停留在主窗口程序和控制台程序 xff0c 虽然QT的新东西QML听过也接触过 xff0c 但是基本上没梳理过调用流程 趁着旧项目要使用QML技术 xff0c 现在就将C 43 43 和QML交互进行总结 目录 一
  • QT下TCP协议实现数据网络传输

    QT开发框架以其跨平台的优势 xff0c 在全世界IT界如雷贯耳 其封装了功能齐全的各种类 xff0c 大大的提高了开发者的效率 本篇内容将介绍如何使用QT 6 4 1框架开发服务器和客户端程序 xff0c 让两端能够首发消息 xff0c
  • 从零实现vins-mono+fast-planner+M100无人机实验在现实场景中的应用

    版权声明 本文为博主原创文章 未经博主允许不能随意转载 本文链接 https blog csdn net AnChenliang 1002 article details 109535355 最近由于科研的需要 要将VINS mono与fa
  • Linux下C语言实现HTTP文件服务器和TCP协议实现网络数据传输

    在实际开发中经常用到web框架 xff0c 比如Servlet xff0c SpringBoot等 xff0c 这些开发框架提高了我们的开发效率 xff0c 节省了开发时间 但是这会令我们技术人员处于浮云之上 xff0c 看不到其本质 说实
  • Linux下C语言UDP协议通信实践

    UDP和TCP协议一样 xff0c 都是传输层协议 是无连接的 xff0c 不安全的 xff0c 报式传输层协议 xff0c 通信过程默认也是阻塞的 其通信特点主要如下 xff1a xff08 1 xff09 不需要建立连接 xff0c 所
  • Ubuntu下PyQt5使用总结

    因为工作中需要给交付团队开发桌面工具 xff0c 考虑到交付团队多使用Mac xff0c 调研了一下发现PyQt5可以实现跨平台 xff0c 满足工具开发需要 xff0c 就用其开发了桌面工具 现以ubuntu开发环境为例总结一下开发过程
  • ubuntu下安装配置grpc

    目录 1 准备环境 2 安装protobuf 3 安装cares库 3 安装grpc 1 17 x 1 准备环境 sudo apt get install pkg config sudo apt get install autoconf a
  • cmake管理子程序,lib库和so库应用实践

    cmake在管理大型项目时经常被用到 xff0c 本文以简单程序演示来说明camke管理项目应用 xff0c 其中包括主程序 xff0c 子程序 xff0c so库程序 xff0c lib程序 目录 1 程序目录结构 2 编译执行 3 清除
  • GIt常用命令总结

    目录 1 创建新建分支 2 强制拉去代码 3 合并相邻提交 xff0c 保证只有一个commit信息 4 本地回退 5 查看git修改列表 6 提交代码 7 切换新分支并从服务端拉取最新 8 git cherry pick合并代码使用 9
  • Linux 下I/O多路复用总结

    xfeff xfeff select xff0c poll xff0c epoll都是IO多路复用的机制 I O多路复用就通过一种机制 xff0c 可以监视多个描述符 xff0c 一旦某个描述符就绪 xff08 一般是读就绪或者写就绪 xf
  • WAV文件头分析

    WAV语音文件头部含有44字节的标志信息 xff0c 其含义如下 xff1a ckid xff1a 4字节 RIFF 标志 xff0c 大写 wavHeader 0 61 39 R 39 wavHeader 1 61 39 I 39 wav
  • Linux环境下限制网速和取消限制网速

    查看网卡信息 ip addr root 64 rabbitmq01 ip addr 1 lo lt LOOPBACK UP LOWER UP gt mtu 65536 qdisc noqueue state UNKNOWN qlen 1 l
  • Linux 网络编程2 TCP并发服务器

    Linux 网络编程学习 TCP IP网络编程2 TCP多线程服务器TCP多进程服务器 在前面TCP网络编程代码的基础上进行改造 xff0c 实现并发服务器功能 TCP多线程服务器 实现功能 xff1a server端可以绑定在任意IP端s
  • HTTP Digest authentication

    什么是摘要认证 摘要认证 xff08 Digest authentication xff09 是一个简单的认证机制 xff0c 最初是为HTTP协议开发的 xff0c 因而也常叫做HTTP摘要 xff0c 在RFC2617中描述 其身份验证