Digest认证详解


Digest认证

最近新项目中对接第三方接口遇到Authorization Digest认证方式,跟以往的token认证不太一样Authorization Digest需要请求服务器两次;

通过postman发送请求:

我们可以看到postman发送了两次请求,我们可以看看两次请求的内容:

可以到看第一次请求返回状态是401并且携带了WWW-Authenticate响应头,WWW-Authenticate响应头的值是第二次请求Authorization请求头中所需要的;

要想知道Authorization Digest认证做了什么我们首先得知道WWW-Authenticate响应头和Authorization请求头中值的含义:

WWW-Authenticate:Digest realm="xxx", qop="auth", nonce="10ba923b57a69919930000da2a75d34b", opaque=""

realm: 表示Web服务端中受保护文档的安全域(域名)
qop: 保护质量,包含auth(默认的)和auth-int(增加了报文完整性检测)两种策略,(可以为空,但是)不推荐为空值
nonce: 服务端向客户端发送质询时附带的一个随机字符串
opaque: 默认为空字符串就好,貌似没什么作用
	
Authorization: Digest username="x", realm="xxx", nonce="10ba923b57a69919930000da2a75d34b", uri="/v3/tokens", algorithm="MD5", qop=auth, nc=00000001, cnonce="0W9VHP6H", response="e57c1968cb4bdd2b854b61a12d429f58"

username: 用户名
realm: 认证域名,来至WW-Authenticate的realm
nonce: 服务端的随机字符串,来至WW-Authenticate的nonce
uri: 请求资源位置
algorithm:加密算法,默认是MD5
qop:保护质量,来至WW-Authenticate的qop
nc:1nonce计数器,是一个16进制的数值,表示同一nonce下客户端发送出请求的数量。例如,在响应的第一个请求中,客户端将发送nc=00000001。这个指示值的目的是让服务端保持这个计数器的一个副本,以便检测重复的请求
cnonce:客户端随机字符串,类似于MD5的salt加密
response:客户端根据算法算出的摘要值,服务器认证就是比较这个值

通过以上的值的含义我们可以看到其中最重要的就是如何计算Authorizationresponse的值,这样我们才能成功请求到服务端;

若算法是:MD5 或者是未指定
则 A1= <username>:<realm>:<passwd>

若 qop 未定义或者 auth:
则 A2= <request-method>:<digest-uri-value>

若 qop 为 auth
      response=MD5(MD5(A1):<nonce>:<nc>:<cnonce>:<qop>:MD5(A2)) 
若 qop 没有定义
      response=MD5(MD5(A1):<nonce>:MD5(A2))

根据以上算法编写DigestUtil:

/**
 * @description: 摘要认证工具类
 * @Title: DigestUtil
 * @Author xlw
 * @Package org.test.util
 * @Date 2024/5/24 22:44
 */
public class DigestUtil {

	/*获取response值*/
    public static String getResponse(
            String username,
            String realm,
            String password,
            String nonce,
            String nc,
            String cnonce,
            String qop,
            String method,
            String uri
    ) {
        String A1 = username + ":" + realm + ":" + password;
        byte[] md5ByteA1 = md5(A1.getBytes());
        String HA1 = new String(Hex.encodeHex(md5ByteA1));

        String A2 = method + ":" + uri;
        byte[] md5ByteA2 = md5(A2.getBytes());
        String HA2 = new String(Hex.encodeHex(md5ByteA2));


        String original = HA1 + ":" + (nonce + ":" + nc + ":" + cnonce + ":" + qop) + ":" + HA2;
        // String original = HA1 + ":" + nonce + ":" + HA2;
        byte[] md5ByteOriginal = md5(original.getBytes());
        String response = new String(Hex.encodeHex(md5ByteOriginal));
        return response;
    }
	
    /*格式化认证*/
    public static String getAuthorization(String username, String realm, String nonce, String uri, String qop, String nc, String cnonce, String response, String opaque) {
        String authorization = "Digest username=\"" + username + "\"" +
                ",realm=\"" + realm + "\"" +
                ",nonce=\"" + nonce + "\"" +
                ",uri=\"" + uri + "\"" +
                ",qop=\"" + qop + "\"" +
                ",nc=\"" + nc + "\"" +
                ",cnonce=\"" + cnonce + "\"" +
                ",response=\"" + response + "\"" +
                ",opaque=\"" + opaque + "\"";
        return authorization;
    }


    /**
     * MD5加密
     */
    public static byte[] md5(byte[] input) {
        return digest(input, "MD5", null, 1);
    }

    /**
     * 对字符串进行散列, 支持md5与sha1算法.
     */
    private static byte[] digest(byte[] input, String algorithm, byte[] salt, int iterations) {
        try {
            MessageDigest digest = MessageDigest.getInstance(algorithm);

            if (salt != null) {
                digest.update(salt);
            }

            byte[] result = digest.digest(input);

            for (int i = 1; i < iterations; i++) {
                digest.reset();
                result = digest.digest(result);
            }
            return result;
        } catch (GeneralSecurityException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        //请求URL
        String url = "http://120.0.0.1:9090/v3/tokens";
        //用户名
        String userName = "xlw";
        //密码
        String password = "xlw123";
        String uri = "/v3/tokens";
        //第一次请求
        HttpResponse response1 = HttpRequest.get(url).contentType("application/json; charset=UTF-8").execute();
        if (HttpStatus.HTTP_UNAUTHORIZED == response1.getStatus()) {
            String authenticate = response1.headers().get("WWW-Authenticate").get(0);
            System.out.println(authenticate);
            String[] children = authenticate.split(",");
            String realm = null, qop = null, nonce = null, opaque = "", method = "GET";
            for (int i = 0; i < children.length; i++) {
                String item = children[i];
                String[] itemEntry = item.split("=");
                if (itemEntry[0].trim().equals("Digest realm")) {
                    realm = itemEntry[1].trim().replaceAll("\"", "");
                } else if (itemEntry[0].trim().equals("qop")) {
                    qop = itemEntry[1].trim().replaceAll("\"", "");
                } else if (itemEntry[0].trim().equals("nonce")) {
                    nonce = itemEntry[1].trim().replaceAll("\"", "");
                }
            }
            String nc = "00000001";
            String cnonce = RandomUtil.randomString(8); //DigestUtil.generateSalt2(8);
            String response = DigestUtil.getResponse(userName, realm, password, nonce, nc, cnonce, qop, method, uri);
            String authorization = DigestUtil.getAuthorization(userName, realm, nonce, uri, qop, nc, cnonce, response, opaque);
            System.out.println(authorization);

            // 第二次请求
            HttpResponse response2 = HttpRequest.get(url).contentType("application/json; charset=UTF-8").header("Authorization", authorization).execute();
            if (HttpStatus.HTTP_OK == response2.getStatus()) {
                //此处摘要认证成功完成
                System.out.println(response2.body());
            }

        }
    }
}

文章作者: 威@猫
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 威@猫 !
评论