在hkws开发中,Digest Auth认证遇到的问题

25 阅读2分钟

hkws提供的demo中的HTTPClientUtil发起的GET/POST/PUT请求都是只发送一次请求,面对Digest Auth永远只能收到401权限不足的反馈,所以需要修改GET/POST/PUT请求的发送方式,以doPut为例:

```
/**
 * 执行带 Digest 认证的 PUT 请求
 */
public static String doPut(DeviceInfoDTO deviceInfo, String url, String xmlBody,String ContentType) {
    String fullUrl = "http://" + deviceInfo.getDevIp() + url;
    String response = "";

    try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
        // 第一次尝试:发送不带认证的请求以获取挑战信息
        HttpPut httpPut = new HttpPut(fullUrl);
        setCommonHeaders(httpPut, deviceInfo,ContentType);
        httpPut.setEntity(new StringEntity(xmlBody, StandardCharsets.UTF_8));

        try (CloseableHttpResponse firstResponse = httpClient.execute(httpPut)) {
            int statusCode = firstResponse.getStatusLine().getStatusCode();

            if (statusCode == 401) {
                // 提取 WWW-Authenticate 头中的 realm 和 nonce
                String wwwAuth = firstResponse.getFirstHeader("WWW-Authenticate").getValue();
                DigestChallenge challenge = parseDigestChallenge(wwwAuth);

                if (challenge != null) {
                    // 第二次尝试:使用 Digest 认证重新发送请求
                    HttpPut authPut = new HttpPut(fullUrl);
                    setCommonHeaders(authPut, deviceInfo,ContentType);
                    authPut.setEntity(new StringEntity(xmlBody, StandardCharsets.UTF_8));

                    // 计算并设置 Digest 认证头
                    String authHeader = calculateDigestAuthHeader(
                            deviceInfo.getUsername(),
                            deviceInfo.getPassword(),
                            "PUT",
                            url,
                            challenge
                    );
                    authPut.setHeader("Authorization", authHeader);

                    try (CloseableHttpResponse authResponse = httpClient.execute(authPut)) {
                        if (authResponse.getEntity() != null) {
                            response = EntityUtils.toString(authResponse.getEntity());
                            System.out.println("认证后响应状态: " + authResponse.getStatusLine());
                            System.out.println("响应内容: " + response);
                        }
                    }
                }
            } else if (statusCode == 200) {
                // 直接成功(不需要认证)
                if (firstResponse.getEntity() != null) {
                    response = EntityUtils.toString(firstResponse.getEntity());
                }
            } else {
                System.out.println("Unexpected status: " + statusCode);
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }

    return response;
}
```

另附

```
```

/**
 * 设置通用的请求头
 */
private static void setCommonHeaders(HttpPut httpPut, DeviceInfoDTO deviceInfo,String ContentType) {
    httpPut.setHeader("Content-Type", ContentType);
    httpPut.setHeader("Accept", "*/*");
    httpPut.setHeader("Host", deviceInfo.getDevIp());
    httpPut.setHeader("Connection", "keep-alive");
    httpPut.setHeader("User-Agent", "HttpClient/4.5.2");
}

/**
 * 设置通用的请求头
 */
private static void setCommonHeaders(HttpRequestBase request, DeviceInfoDTO deviceInfo) {
    request.setHeader("Accept", "*/*");
    request.setHeader("Host", deviceInfo.getDevIp());
    request.setHeader("Connection", "keep-alive");
    request.setHeader("User-Agent", "HttpClient/4.5.2");

    // 根据请求类型设置 Content-Type
    if (request instanceof HttpPut || request instanceof HttpPost) {
        request.setHeader("Content-Type", "application/xml");
    }
}

/**
 * 解析 Digest 认证挑战
 */
private static DigestChallenge parseDigestChallenge(String wwwAuthenticate) {
    Pattern realmPattern = Pattern.compile("realm="([^"]+)"");
    Pattern noncePattern = Pattern.compile("nonce="([^"]+)"");
    Pattern algorithmPattern = Pattern.compile("algorithm=([^,]+)");
    Pattern qopPattern = Pattern.compile("qop="([^"]+)"");

    Matcher realmMatcher = realmPattern.matcher(wwwAuthenticate);
    Matcher nonceMatcher = noncePattern.matcher(wwwAuthenticate);
    Matcher algorithmMatcher = algorithmPattern.matcher(wwwAuthenticate);
    Matcher qopMatcher = qopPattern.matcher(wwwAuthenticate);

    if (realmMatcher.find() && nonceMatcher.find()) {
        DigestChallenge challenge = new DigestChallenge();
        challenge.realm = realmMatcher.group(1);
        challenge.nonce = nonceMatcher.group(1);
        challenge.algorithm = algorithmMatcher.find() ? algorithmMatcher.group(1) : "MD5";
        challenge.qop = qopMatcher.find() ? qopMatcher.group(1) : "auth";
        return challenge;
    }

    return null;
}

/**
 * 计算 Digest 认证头
 */
private static String calculateDigestAuthHeader(String username, String password,
                                                String method, String uri,
                                                DigestChallenge challenge) {
    // 生成客户端随机数和计数器
    String cnonce = generateCnonce();
    String nc = "00000001"; // 每次请求递增

    try {
        // HA1 = MD5(username:realm:password)
        String ha1 = md5(username + ":" + challenge.realm + ":" + password);

        // HA2 = MD5(method:uri)
        String ha2 = md5(method + ":" + uri);

        // response = MD5(HA1:nonce:nc:cnonce:qop:HA2)
        String response = md5(ha1 + ":" + challenge.nonce + ":" + nc + ":" + cnonce + ":" + challenge.qop + ":" + ha2);

        // 构建 Authorization 头
        return String.format("Digest username="%s", realm="%s", nonce="%s", " +
                        "uri="%s", algorithm="%s", qop="%s", nc=%s, " +
                        "cnonce="%s", response="%s"",
                username, challenge.realm, challenge.nonce, uri,
                challenge.algorithm, challenge.qop, nc, cnonce, response);
    } catch (Exception e) {
        throw new RuntimeException("Failed to calculate digest auth", e);
    }
}

/**
 * 生成客户端随机数
 */
private static String generateCnonce() {
    // 使用当前时间戳和随机数生成更安全的 cnonce
    long timestamp = System.currentTimeMillis();
    int random = (int) (Math.random() * 1000000);
    return md5(timestamp + ":" + random).substring(0, 16);
}

/**
 * MD5 计算
 */
private static String md5(String input) {
    try {
        MessageDigest md = MessageDigest.getInstance("MD5");
        byte[] digest = md.digest(input.getBytes(StandardCharsets.UTF_8));
        return HexFormat.of().formatHex(digest);
    } catch (Exception e) {
        throw new RuntimeException("MD5 calculation failed", e);
    }
}

/**
 * Digest 挑战信息容器类
 */
static class DigestChallenge {
    String realm;
    String nonce;
    String algorithm;
    String qop;
}
```
```