Fork me on GitHub

HTTP Digest

Http Digest

简介

摘要访问认证最初由RFC 2069中被定义。RFC 2069大致定义了一个传统的由服务器生成随机数来维护安全性的摘要认证架构。

image

第一次访问响应401,服务器生成nonce参数,nonce位于响应头WWW-Authenticate中。

image

第二次访问根据nonce计算出response,放在请求头Authorization中。

image

response认证响应

认证响应由下列组成(HA1、HA2、A1、及A2为字符串变量的名称):

身份验证最初是由RFC 2069(An Extension to HTTP: Digest Access Authentication),身份验证响应的格式
HA1 = MD5(username:realm:password)
HA2 = MD5(method:digestURI)
response = MD5(HA1:nonce:HA2)

MD5哈希是一个16字节的值。用于计算响应的HA1和HA2值分别是MD5哈希的十六进制表示形式(小写,这里一定要是小写)。

RFC 2069后来被RFC 2617( HTTP Authentication: Basic and Digest Access Authentication )取代。RFC 2617引入了一些可选的安全性增强功能以摘要式身份验证。 quality of protection (qop,保护质量),客户端随机数计数器增加(nc),以及客户端生成的随机数(cnonce)。这些增强功能可以用来防止明文攻击、密码分析等。

image

如果 algorithm 的值为“MD5”或未指定,则HA1为

HA1 = MD5(username:realm:password)

如果 algorithm 的值为“ MD5-sess”,则HA1为

HA1 = MD5(MD5(username:realm:password):nonce:cnonce)

如果qop指令的值为“auth”或未指定,则HA2为

HA2 = MD5(method:digestURI)

如果qop指令的值为“auth-int”,则HA2为

HA2 = MD5(method:digestURI:MD5(entityBody))

如果qop指令的值为“auth”或“auth-int”,则response计算方式:

response = MD5(HA1:nonce:nonceCount:cnonce:qop:HA2)

如果未指定qop指令,则response计算方式:

response = MD5(HA1:nonce:HA2)

认证总结

algorithm:md5 qop:auth

HA1 = MD5(username:realm:password)
HA2 = MD5(method:digestURI)
response = MD5(HA1:nonce:nonceCount:cnonce:qop:HA2)

JAVA代码

计算response,algorithm:md5 qop:auth

String username = "Mufasa";
String password = "Circle Of Life";
String realm = "testrealm@host.com";
String nonce = "dcd98b7102dd2f0e8b11d0f600bfb0c093";  //服务器端返回的随机数
String nonceCount = "00000001";
String cnonce = "0a4f113b";
String qop = "auth";
String method = "GET";
String degistURI = "/dir/index.html";

String HA1 = MD5Util.MD5(username+":"+realm+":"+password).toLowerCase();  //小写的md5
String HA2 = MD5Util.MD5(method+":"+degistURI).toLowerCase();

String resoristr = HA1+":"+nonce+":"+nonceCount+":"+cnonce+":"+qop+":"+HA2;
String responseStr = MD5Util.MD5(resoristr).toLowerCase();
System.out.println("response值为::::   "+responseStr);

工具类

Maven坐标

<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>3.14.2</version>
</dependency>

image

package com.autumn.util;

import com.autumn.system.tools.MD5Util;
import okhttp3.*;

import java.io.BufferedReader;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * HttpDigest
 *
 * @author: 秋雨
 * 2021-04-09 15:33
 **/
public class HttpDigest {


    /**
     * 获取响应头
     * @param urlString
     * @param method
     * @param params
     * @param requestProperty
     * @param request_transencode
     * @param response_transencode
     * @param response_decode
     * @return
     * @throws Exception
     */
    public static Map<String,String> getResHeaderByUrl(String urlString, String method, String params, Map<String,String> requestProperty, String request_transencode, String response_transencode, String response_decode)
            throws Exception {
        BufferedReader in = null;
        java.net.HttpURLConnection conn = null;
        Map<String,String> responseHeader = new HashMap<String,String>();// 保存调用http服务后的响应头
        try {
            java.net.URL url = new java.net.URL(urlString);
            conn = (java.net.HttpURLConnection) url.openConnection();
            conn.setRequestMethod(method.toUpperCase());// 请求的方法get,post,put,delete
            conn.setConnectTimeout(5 * 1000);// 设置连接超时时间为5秒
            conn.setReadTimeout(20 * 1000);// 设置读取超时时间为20秒
            conn.setDoOutput(true);// 使用 URL 连接进行输出,则将 DoOutput标志设置为 true

            //从google浏览器请求地址中的cookie里面找到这个
            conn.setRequestProperty("Accept-Charset", "UTF-8");
            conn.setRequestProperty("accept", "*/*");
            conn.setRequestProperty("connection", "Keep-Alive");
            conn.setRequestProperty("User-Agent".toLowerCase(), "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36");

            /*添加头信息*/
            if (!(requestProperty == null || requestProperty.isEmpty())){
                for (Map.Entry<String,String> entry:requestProperty.entrySet()){
                    conn.setRequestProperty(entry.getKey(),entry.getValue());
                }
            }

            if (request_transencode == null || request_transencode.trim().length() == 0){
                request_transencode = "utf-8";
            }

            if (!(params == null || params.trim().length() == 0)) {
                OutputStream outStream = conn.getOutputStream();// 返回写入到此连接的输出流
                outStream.write(params.getBytes(request_transencode));  //不指定参数编码方式是默认用jvm的编码方式
                outStream.close();// 关闭流
            }

            //获取响应头
            Map headers = conn.getHeaderFields();
            Set<String> keys = headers.keySet();
            for( String key : keys ){
                String val = conn.getHeaderField(key);
                //System.out.println(key+":       "+val);
                responseHeader.put(key,val);
            }
        } catch (Exception ex) {
            throw new Exception("发送http请求失败",ex);
        } finally {
            if (null != in) {
                in.close();
            }
            if (null != conn) {
                conn.disconnect();
            }
        }
        return responseHeader;
    }

    /**
     * 计算http digest md5认证的response参数
     * @param username 用户名
     * @param password 密码
     * @param realm 用户域
     * @param nonce 随机数 服务器生成
     * @param qop 保护质量  auth:鉴权,不对消息体做完整性验证。 auth-int:鉴权并需要对消息体做摘要,保证消息完整性。
     * @param nonceCount nonce计数参数,以00000001开始,每次请求加1,目的是防止重放攻击
     * @param cnonce 客户端nonce
     * @param method 请求方式GET/POST
     * @param digestURI 请求的URI
     * @return
     */
    public static String getDigestResponseStr(String username,String password,String realm,String nonce,String qop,
                                              String nonceCount,String cnonce,String method,String digestURI){
        String HA1 = MD5Util.MD5(username+":"+realm+":"+password).toLowerCase();
        String HA2 = MD5Util.MD5(method+":"+digestURI).toLowerCase();
        String responseStr = MD5Util.MD5(HA1+":"+nonce+":"+nonceCount+":"+cnonce+":"+qop+":"+HA2).toLowerCase();
        return responseStr;
    }

    /**
     * 以application/json方式请求http digest,qop为auth模式
     * @param url 请求url
     * @param jsonBody json字符串(post的body部分)
     * @param username 用户名
     * @param password 密码
     * @param digestURI 请求的URI
     * @return 返回请求的主体值
     * @throws Exception
     */
    public static String postJsonByHttpMd5Digest(String url,String jsonBody,String username,String password,String digestURI) throws Exception {
        //1.直接请求获取nonce
        Map<String,String> responseHeader = getResHeaderByUrl(url, "POST", null, null, null, null, null);
        //获取401的授权头WWW-Authenticate    Digest realm=realm@host.com,qop=auth,nonce=RL42UmdJfnhIX5Vd8AXNlcThlwk76iXOlA,opaque=Dpym82zlCll1LYFEDzEAXubjGUSkqmKEx6
        String authenticate = responseHeader.get("WWW-Authenticate");
        String[] authenticateArr = authenticate.replace("Digest", "").replace(" ", "").split(",");
        //获取授权头中的nonce、realm等值
        Map<String,String> reqMap = new HashMap();
        if (authenticateArr!=null && authenticateArr.length>0){
            for (String d:authenticateArr){
                String[] headerArr = d.split("=");
                reqMap.put(headerArr[0],headerArr[1]);
            }
        }
        //2.获取到nonce值成功
        //System.out.println("nonce值为::::   "+reqMap.get("nonce"));

        //3.计算获取response值
        String realm = reqMap.get("realm");
        String nonce = reqMap.get("nonce");
        String qop = "auth";
        String nonceCount = "";  //nc计数器
        String cnonce = "";  //随机字符串
        String method = "POST";

        String digestResponseStr = getDigestResponseStr(username, password, realm, nonce, qop, nonceCount, cnonce, method, digestURI);

        //4.请求http
        OkHttpClient client = new OkHttpClient();
        MediaType mediaType = MediaType.parse("application/json");
        RequestBody body = RequestBody.create(mediaType, jsonBody);
        Request request = new Request.Builder()
                .url(url)
                .post(body)
                .addHeader("authorization", "Digest username=\""+username+"\", realm=\""+realm+"\", " +
                        "nonce=\""+nonce+"\", uri=\""+digestURI+"\", " +
                        "qop=auth, nc=\"\", cnonce=\"\", " +
                        "response=\""+digestResponseStr+"\", opaque=\"\"")
                .addHeader("content-type", "application/json")
                .addHeader("cache-control", "no-cache")
                .build();
        Response response = client.newCall(request).execute();

        /*System.out.println("Response code: " + response.code());
        System.out.println("Response Header: " + response.headers());
        System.out.println("Response Body: " +response.body().string());*/

        String reulst = response.body().string();
        return reulst;
    }


    public static void main(String[] args) throws Exception {
        String url = "http://IP:PORT/url";
        String jsonBody = "{}";
        String username = "username";
        String password = "password";
        String uri = "/url";

        String r = HttpDigest.postJsonByHttpMd5Digest(url, jsonBody, username, password, uri);
        System.out.println(r);
    }
}
posted @ 2021-04-09 16:04  秋夜雨巷  阅读(652)  评论(0编辑  收藏  举报