DigestAuthenticationHelper 摘要认证工具类封装

This commit is contained in:
shikong 2023-09-26 01:50:50 +08:00
parent 309062faa1
commit 57b04fdcf3
4 changed files with 333 additions and 1 deletions

View File

@ -26,6 +26,12 @@
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-crypto</artifactId>
<version>${hutool.version}</version>
</dependency>
<!-- Jackson -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>

View File

@ -0,0 +1,285 @@
package cn.skcks.docking.gb28181.sip.utils;
import cn.hutool.core.util.IdUtil;
import cn.hutool.crypto.digest.DigestAlgorithm;
import cn.hutool.crypto.digest.DigestUtil;
import cn.skcks.docking.gb28181.sip.generic.SipBuilder;
import lombok.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import javax.sip.SipFactory;
import javax.sip.address.SipURI;
import javax.sip.address.URI;
import javax.sip.header.AuthorizationHeader;
import javax.sip.header.WWWAuthenticateHeader;
import javax.sip.message.Request;
import javax.sip.message.Response;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.Random;
@Slf4j
public class DigestAuthenticationHelper {
private static final MessageDigest messageDigest;
public static final String DEFAULT_ALGORITHM = "MD5";
public static final String DEFAULT_SCHEME = "Digest";
/** to hex converter */
private static final char[] toHex = { '0', '1', '2', '3', '4', '5', '6',
'7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
/**
* Default constructor.
* @throws NoSuchAlgorithmException
*/
static{
try {
messageDigest = MessageDigest.getInstance(DEFAULT_ALGORITHM);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
public static String toHexString(byte[] b) {
int pos = 0;
char[] c = new char[b.length * 2];
for (byte value : b) {
c[pos++] = toHex[(value >> 4) & 0x0F];
c[pos++] = toHex[value & 0x0f];
}
return new String(c);
}
/**
* Generate the challenge string.
*
* @return a generated nonce.
*/
private static String generateNonce() {
long time = Instant.now().toEpochMilli();
Random rand = new Random();
long pad = rand.nextLong();
String nonceString = Long.valueOf(time).toString() + Long.valueOf(pad).toString();
byte[] mdBytes = messageDigest.digest(nonceString.getBytes());
return toHexString(mdBytes);
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class Authentication {
private String realm;
private String userName;
}
@SneakyThrows
public static WWWAuthenticateHeader generateChallenge(String realm) {
WWWAuthenticateHeader proxyAuthenticate = SipBuilder.getHeaderFactory().createWWWAuthenticateHeader(DEFAULT_SCHEME);
proxyAuthenticate.setParameter("realm", realm);
proxyAuthenticate.setParameter("qop", "auth");
proxyAuthenticate.setParameter("nonce", generateNonce());
proxyAuthenticate.setParameter("algorithm", DEFAULT_ALGORITHM);
return proxyAuthenticate;
}
public static void generateChallenge(Response response, String realm) {
response.setHeader(generateChallenge(realm));
}
/**
* Authenticate the inbound request.
*
* @param request - the request to authenticate.
* @param hashedPassword -- the MD5 hashed string of username:realm:plaintext password.
*
* @return true if authentication succeded and false otherwise.
*/
public static boolean doAuthenticateHashedPassword(Request request, String hashedPassword) {
AuthorizationHeader authHeader = (AuthorizationHeader) request.getHeader(AuthorizationHeader.NAME);
if ( authHeader == null ) {
return false;
}
String realm = authHeader.getRealm();
String username = authHeader.getUsername();
if ( username == null || realm == null ) {
return false;
}
String nonce = authHeader.getNonce();
URI uri = authHeader.getURI();
if (uri == null) {
return false;
}
String A2 = request.getMethod().toUpperCase() + ":" + uri;
String HA1 = hashedPassword;
byte[] mdbytes = messageDigest.digest(A2.getBytes());
String HA2 = toHexString(mdbytes);
String cnonce = authHeader.getCNonce();
String KD = HA1 + ":" + nonce;
if (cnonce != null) {
KD += ":" + cnonce;
}
KD += ":" + HA2;
mdbytes = messageDigest.digest(KD.getBytes());
String mdString = toHexString(mdbytes);
String response = authHeader.getResponse();
return mdString.equals(response);
}
/**
* Authenticate the inbound request given plain text password.
*
* @param request - the request to authenticate.
* @param password -- the plain text password.
*
* @return true if authentication succeded and false otherwise.
*/
public static boolean doAuthenticatePlainTextPassword(Request request, String password) {
AuthorizationHeader authorizationHeader = (AuthorizationHeader) request.getHeader(AuthorizationHeader.NAME);
if ( authorizationHeader == null || authorizationHeader.getRealm() == null) {
return false;
}
return doAuthenticatePlainTextPassword(request.getMethod().toUpperCase(),authorizationHeader,password);
}
public static boolean doAuthenticatePlainTextPassword(String method,AuthorizationHeader authorizationHeader, String password) {
if ( authorizationHeader == null || authorizationHeader.getRealm() == null) {
return false;
}
String realm = authorizationHeader.getRealm().trim();
String username = authorizationHeader.getUsername().trim();
if(ObjectUtils.anyNull(username,realm)){
return false;
}
String nonce = authorizationHeader.getNonce();
URI uri = authorizationHeader.getURI();
if (uri == null) {
return false;
}
// qop 保护质量 包含auth默认的和auth-int增加了报文完整性检测两种策略
String qop = authorizationHeader.getQop();
// 客户端随机数这是一个不透明的字符串值由客户端提供并且客户端和服务器都会使用以避免用明文文本
// 这使得双方都可以查验对方的身份并对消息的完整性提供一些保护
String cnonce = authorizationHeader.getCNonce();
// nonce计数器是一个16进制的数值表示同一nonce下客户端发送出请求的数量
int nc = authorizationHeader.getNonceCount();
String ncStr = String.format("%08x", nc).toUpperCase();
String A1 = String.join(":",username , realm , password);
String A2 = String.join(":", method.toUpperCase() , uri.toString());
byte[] mdbytes = messageDigest.digest(A1.getBytes());
String HA1 = toHexString(mdbytes);
log.debug("A1: " + A1);
log.debug("A2: " + A2);
mdbytes = messageDigest.digest(A2.getBytes());
String HA2 = toHexString(mdbytes);
log.debug("HA1: " + HA1);
log.debug("HA2: " + HA2);
// String cnonce = authorizationHeader.getCNonce();
log.debug("nonce: " + nonce);
log.debug("nc: " + ncStr);
log.debug("cnonce: " + cnonce);
log.debug("qop: " + qop);
String KD = HA1 + ":" + nonce;
if (qop != null && qop.equalsIgnoreCase("auth") ) {
if (nc != -1) {
KD += ":" + ncStr;
}
if (cnonce != null) {
KD += ":" + cnonce;
}
KD += ":" + qop;
}
KD += ":" + HA2;
log.debug("KD: " + KD);
mdbytes = messageDigest.digest(KD.getBytes());
String mdString = toHexString(mdbytes);
log.debug("mdString: " + mdString);
String response = authorizationHeader.getResponse();
log.debug("response: " + response);
return mdString.equals(response);
}
@SneakyThrows
public static AuthorizationHeader createAuthorization(String serverIp, int serverPort, String serverId, String deviceId,String password, WWWAuthenticateHeader www){
String hostAddress = SipBuilder.createHostAddress(serverIp, serverPort);
SipURI sipURI = SipBuilder.createSipURI(serverId, hostAddress);
if (www == null) {
AuthorizationHeader authorizationHeader = SipBuilder.getHeaderFactory().createAuthorizationHeader("Digest");
authorizationHeader.setUsername(deviceId);
authorizationHeader.setURI(sipURI);
authorizationHeader.setAlgorithm("MD5");
return authorizationHeader;
}
String realm = www.getRealm();
String nonce = www.getNonce();
String scheme = www.getScheme();
String qop = www.getQop();
String cNonce = null;
int nc = 1;
String ncStr = String.format("%08x", nc).toUpperCase();
if (qop != null) {
if ("auth".equalsIgnoreCase(qop)) {
// 客户端随机数这是一个不透明的字符串值由客户端提供并且客户端和服务器都会使用以避免用明文文本
// 这使得双方都可以查验对方的身份并对消息的完整性提供一些保护
cNonce = IdUtil.fastSimpleUUID();
}else if ("auth-int".equalsIgnoreCase(qop)){
// TODO
}
}
String HA1 = DigestUtil.md5Hex((deviceId + ":" + realm + ":" + password).getBytes());
String HA2= DigestUtil.md5Hex((Request.REGISTER + ":" + sipURI.toString()).getBytes());
StringBuilder reStr = new StringBuilder();
reStr.append(HA1);
reStr.append(":");
reStr.append(nonce);
reStr.append(":");
if (qop != null) {
reStr.append(ncStr);
reStr.append(":");
reStr.append(cNonce);
reStr.append(":");
reStr.append(qop);
reStr.append(":");
}
reStr.append(HA2);
String response = DigestUtil.md5Hex(reStr.toString().getBytes());
AuthorizationHeader authorizationHeader = SipFactory.getInstance().createHeaderFactory().createAuthorizationHeader(scheme);
authorizationHeader.setUsername(deviceId);
authorizationHeader.setRealm(realm);
authorizationHeader.setNonce(nonce);
authorizationHeader.setURI(sipURI);
authorizationHeader.setResponse(response);
authorizationHeader.setAlgorithm(DigestAlgorithm.MD5.name());
if (qop != null) {
authorizationHeader.setQop(qop);
authorizationHeader.setCNonce(cNonce);
authorizationHeader.setNonceCount(nc);
}
return authorizationHeader;
}
}

View File

@ -1,6 +1,8 @@
package cn.skcks.docking.gb28181.sip.utils;
import cn.hutool.core.lang.id.NanoId;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.RandomUtil;
import gov.nist.javax.sip.address.AddressImpl;
import gov.nist.javax.sip.address.SipUri;
import gov.nist.javax.sip.header.Subject;
@ -20,6 +22,7 @@ import javax.sip.header.FromHeader;
import javax.sip.header.Header;
import javax.sip.header.UserAgentHeader;
import javax.sip.message.Request;
import java.security.SecureRandom;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
@ -29,6 +32,11 @@ import java.util.List;
public class SipUtil {
public static String UserAgent = "GB28181-Docking-Platform";
private static final char[] DEFAULT_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
public static String nanoId(){
return NanoId.randomNanoId(null,DEFAULT_ALPHABET,32);
}
public static String getIdFromFromHeader(FromHeader fromHeader) {
AddressImpl address = (AddressImpl)fromHeader.getAddress();
SipUri uri = (SipUri) address.getURI();
@ -65,7 +73,7 @@ public class SipUtil {
}
public static String generateFromTag(){
return IdUtil.fastSimpleUUID();
return nanoId();
}
public static String generateTag(){

View File

@ -0,0 +1,33 @@
package cn.skcks.docking.gb28181.sip.utils;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import javax.sip.header.AuthorizationHeader;
import javax.sip.header.WWWAuthenticateHeader;
import javax.sip.message.Request;
@Slf4j
public class AuthenticationTest {
public static final String serverId = "44050100002000000001";
public static final String serverIp = "10.10.10.200";
public static final int serverPort = 5060;
public static final String domain = "4405010000";
public static final String deviceId = "44050100001110000010";
@Test
void test() {
AuthorizationHeader authorization = DigestAuthenticationHelper.createAuthorization(serverIp, serverPort, serverId, deviceId, "123456",null);
log.info("\n{}", authorization);
WWWAuthenticateHeader wwwAuthenticateHeader = DigestAuthenticationHelper.generateChallenge(domain);
log.info("\n{}", wwwAuthenticateHeader);
authorization = DigestAuthenticationHelper.createAuthorization(serverIp, serverPort, serverId, deviceId, "123456",wwwAuthenticateHeader);
log.info("\n{}", authorization);
boolean passed = DigestAuthenticationHelper.doAuthenticatePlainTextPassword(Request.REGISTER, authorization, "123456");
log.info("authorization passed {}",passed);
}
}