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:客户端根据算法算出的摘要值,服务器认证就是比较这个值通过以上的值的含义我们可以看到其中最重要的就是如何计算Authorization中response的值,这样我们才能成功请求到服务端;
若算法是: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());
}
}
}
}