HTTP 请求通过增加特定头部信息,为不同的认证协议提供了一个可扩展框架。
步骤 | 首部 | 描述 | 方法状态 |
---|---|---|---|
请求 | 第一条没有认证信息 | GET | |
服务器返回 | WWW-Authenticate | 服务器用 401 状态拒绝了请求,说明需要用户提供用户名和密码鉴权 | GET |
再次请求鉴权 | Authenticate | 客户端重新发出请求,但这一次会附加一个Authorization首部,用来说明认证算法、用户名和密码 | GET |
服务器返回成功 | 服务器返回成功码200 | GET |
对用户名、认证域(realm)以及密码的合并值计算 MD5 哈希值,结果称为 HA1。
对HTTP方法以及URI的摘要的合并值计算 MD5 哈希值,例如,“GET” 和 “/dir/index.html”,结果称为 HA2。
对 HA1、服务器密码随机数(nonce)、请求计数(nc)、客户端密码随机数(cnonce)、保护质量(qop)以及 HA2 的合并值计算 MD5 哈希值。结果即为客户端提供的 response 值。
因为服务器拥有与客户端同样的信息,因此服务器可以进行同样的计算,以验证客户端提交的 response 值的正确性。在上面给出的例子中,结果是如下计算的。 (MD5()表示用于计算 MD5 哈希值的函数;“\”表示接下一行;引号并不参与计算)
根据上面的算法所给出的示例,将在每步得出如下结果。
HA1 = MD5(
HA2 = MD5(
Response = MD5(HA1:
其中reaml、nonce、qop从服务器401时的头信息中获取
method:请求方式,如:“POST”、“GET”等
disgestUriPath:请求的uri ,如下图抓包中的 "/onvif/device_service"
conce:客户端生成的随机数
nc:请求次数
username:登录账号
psd:登录密码
普通的http请求,代码中使用到okhttp网络框架请求
public static byte[] getByteArray2(String url, String user, String psd) throws IOException {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
// .header("Authorization", "Client-ID " + UUID.randomUUID())
.url(url)
.get()
.build();
Response response = client.newCall(request).execute();
if (response.isSuccessful() ) {
if( response.code() == 200 && response.body() != null){
return response.body().bytes();
}
} else if(response.code() == 401){ // 未鉴权去鉴权
//WWW-Authenticate: Digest qop="auth", realm="DS-2CD2310FD-I", nonce="4d6a4931516a51304e554d364e445935595759785954553d", stale="TRUE"
//WWW-Authenticate: Basic realm="DS-2CD2310FD-I"
Headers h = response.headers();
List<String> auths = h.values("WWW-Authenticate");
Pattern qopPattern = Pattern.compile("qop=\"(.*?)\"");
Pattern realmPattern = Pattern.compile("realm=\"(.*?)\"");
Pattern noncePattern = Pattern.compile("nonce=\"(.*?)\"");
String qop = "";
String realm = "";
String nonce = "";
String method = request.method();
String host = response.request().url().host();
String disgestUriPath = url.split(host)[1];
for (String head: auths) {
Matcher qopMatcher = qopPattern.matcher(head);
while (qopMatcher.find()){
try{
qop = qopMatcher.group(1);
} catch (Exception e){
LogClientUtils.d(tag, e.getMessage());
}
}
Matcher realmMatcher = realmPattern.matcher(head);
while (realmMatcher.find()){
try{
realm = realmMatcher.group(1);
} catch (Exception e){
LogClientUtils.d(tag, e.getMessage());
}
}
Matcher nonceMatcher = noncePattern.matcher(head);
while (nonceMatcher.find()){
try{
nonce = nonceMatcher.group(1);
} catch (Exception e){
LogClientUtils.d(tag, e.getMessage());
}
}
}
return degistHttp(url, user, psd, method, disgestUriPath, nonce, realm, qop);
}
return null;
}
增加鉴权信息请求
private static byte[] degistHttp(String url, String user, String psd, String method, String disgestUriPath, String nonce, String realm, String qop) throws IOException {
String nc = "00000001";
String cnonce = getNonce();
String ha1Data = getMd5Data(user, realm, psd);
String ha1 = MD5Util.MD5Encode(ha1Data);
String ha2Data = getMd5Data(method, disgestUriPath);
String ha2 = MD5Util.MD5Encode(ha2Data);
String ha3Data = getMd5Data(ha1, nonce, nc, cnonce, qop, ha2);
String responseData = MD5Util.MD5Encode(ha3Data);
String headFormat = "Digest username=\"%s\",realm=\"%s\",nonce=\"%s\",uri=\"%s\",cnonce=\"%s\",nc=%s,response=\"%s\",qop=\"%s\"" ;
String head = String.format(headFormat, user, realm, nonce, disgestUriPath, cnonce, nc, responseData, qop);
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
// .header("Authorization", "Client-ID " + UUID.randomUUID())
.url(url)
.addHeader("Authorization", head)
.get()
.build();
Response response = client.newCall(request).execute();
if (response.isSuccessful() ) {
if( response.code() == 200 && response.body() != null){
return response.body().bytes();
}
}
return null;
}
private static String getMd5Data(String... params){
StringBuilder sb = new StringBuilder();
for (String param : params){
sb.append(param).append(":");
}
String data = sb.toString();
return data.substring(0, data.length() - 1);
}
/**
* 获取 Nonce
*
* @return Nonce
*/
private static String getNonce() {
//初始化随机数
Random r = new Random();
String text = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
String nonce = "";
for (int i = 0; i < 32; i++) {
int index = r.nextInt(text.length());
nonce = nonce + text.charAt(index);
}
return nonce;
}
第一次不带鉴权数据请求修改onvif摄像头的密码:
hm¼\V~0Z:XEùEV¥@@À¨ À¨çPÀ8Úú¶D×PD
ßPOST /onvif/device_service HTTP/1.1
Host: 192.168.1.20
Content-Type: application/soap+xml; charset=utf-8
Content-Length: 468
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:tds="http://www.onvif.org/ver10/device/wsdl" xmlns:tt="http://www.onvif.org/ver10/schema">
<soap:Body>
<tds:SetUser>
<tds:User>
<tt:Username>admin</tt:Username>
<tt:Password>admin123</tt:Password>
<tt:UserLevel>Administrator</tt:UserLevel>
</tds:User>
</tds:SetUser>
</soap:Body>
</soap:Envelope>
服务器返回401
0Z:XEùhm¼\V~Eê @@ÊÚÀ¨À¨ Pçú¶D×À8ÜmPÛS/HTTP/1.1 401 Unauthorized
Date: Tue, 29 Oct 2019 10:54:50 GMT
Server: webserver
Content-Length: 218
Content-Type: text/html
Connection: close
WWW-Authenticate: Digest qop="auth", realm="DS-2CD2310FD-I", nonce="4e555130516a6b304e546f784e7a49334e7a417a59513d3d"
<!DOCTYPE html>
<html><head><title>Document Error: Unauthorized</title></head>
<body><h2>Access Error: 401 -- Unauthorized</h2>
<p>Authentication Error: Access Denied! Authorization required.</p>
</body>
</html>
增加鉴权数据再次请求:
hm¼\V~0Z:XEùEVª@@À¨ À¨çPkeqâhPDøPOST /onvif/device_service HTTP/1.1
Host: 192.168.1.20
Content-Type: application/soap+xml; charset=utf-8
Authorization: Digest username="admin", realm="DS-2CD2310FD-I", qop="auth", algorithm="MD5", uri="/onvif/device_service", nonce="4e555130516a6b304e546f784e7a49334e7a417a59513d3d", nc=00000001, cnonce="0EE3ED23BFD9A00B2AB542E3BAB85BDB", response="518fd6d1666f9f00a5c5097359188c4e"
Content-Length: 468
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:tds="http://www.onvif.org/ver10/device/wsdl" xmlns:tt="http://www.onvif.org/ver10/schema">
<soap:Body>
<tds:SetUser>
<tds:User>
<tt:Username>admin</tt:Username>
<tt:Password>admin123</tt:Password>
<tt:UserLevel>Administrator</tt:UserLevel>
</tds:User>
</tds:SetUser>
</soap:Body>
</soap:Envelope>
服务器鉴权成功返回200
0Z:XEùhm¼\V~EíL$@@hyÀ¨À¨ PçqèkÕPôYÌ"http://docs.oasis-open.org/wsrf/bf-2"
xmlns:wsntw="http://docs.oasis-open.org/wsn/bw-2" xmlns:wsrf-rw="http://docs.oasis-open.org/wsrf/rw-2"
xmlns:wsaw="http://www.w3.org/2006/05/addressing/wsdl" xmlns:wsrf-r="http://docs.oasis-open.org/wsrf/r-2"
xmlns:trc="http://www.onvif.org/ver10/recording/wsdl" xmlns:tse="http://www.onvif.org/ver10/search/wsdl"
xmlns:trp="http://www.onvif.org/ver10/replay/wsdl" xmlns:tnshik="http://www.hikvision.com/2011/event/topics"
xmlns:hikwsd="http://www.onvifext.com/onvif/ext/ver10/wsdl" xmlns:hikxsd="http://www.onvifext.com/onvif/ext/ver10/schema" xmlns:tas="http://www.onvif.org/ver10/advancedsecurity/wsdl"><env:Body><tds:SetUserResponse/>
</env:Body>
</env:Envelope>
从上面我们可以看到,倘若每一次post访问都需要鉴权的话,那么我们都需要进行两次网络访问。效率明显比较低,这个时候ONVIF的令牌验证方式,该方式可直接把鉴权信息写在请求体当中,一次网络访问即可解决战斗。算法如下
Digest = B64ENCODE( SHA1( B64DECODE( Nonce ) + Date + Password ) )
Nonce :随机数
Date:当前时间(格式: 2010-09-16T07:50:45Z)
Password :登录密码
Resulting Digest – tuOSpGlFlIXsozq4HFNeeGeFLEI=
生成 Digest
package com.wp.android_onvif.util;
import android.util.Base64;
import com.wp.android_onvif.onvifBean.Digest;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Random;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class Gsoap {
/**
* Digest = B64ENCODE( SHA1( B64DECODE( Nonce ) + Date + Password ) )
* For example:
* Nonce – LKqI6G/AikKCQrN0zqZFlg==
* Date – 2010-09-16T07:50:45Z
* Password – userpassword
* Resulting Digest – tuOSpGlFlIXsozq4HFNeeGeFLEI=
* 生成 Digest
*/
public static Digest getDigest(String userName, String psw) {
Digest digest = new Digest();
String nonce = getNonce();
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss'Z'",
Locale.getDefault());
String time = df.format(new Date());
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
// nonce需要用Base64解码一次
byte[] b1 = Base64.decode(nonce.getBytes(), Base64.DEFAULT);
// 生成字符字节流
byte[] b2 = time.getBytes(); // "2018-01-10T11:00:00Z";
byte[] b3 = psw.getBytes();
// 根据我们传得值的长度生成流的长度
byte[] b4;
// 利用sha-1加密字符
md.update(b1, 0, b1.length);
md.update(b2, 0, b2.length);
md.update(b3, 0, b3.length);
// 生成sha-1加密后的流
b4 = md.digest();
// 生成最终的加密字符串
String result = new String(Base64.encode(b4, Base64.DEFAULT)).trim();
// Log.e("Gsoap", result);
digest.setNonce(nonce);
digest.setCreatedTime(time);
digest.setUserName(userName);
digest.setEncodePsw(result);
return digest;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取 Nonce
*
* @return Nonce
*/
private static String getNonce() {
//初始化随机数
Random r = new Random();
String text = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
String nonce = "";
for (int i = 0; i < 32; i++) {
int index = r.nextInt(text.length());
nonce = nonce + text.charAt(index);
}
return nonce;
}
}
修改onvif摄像头密码请求数据体格式如下:
将我们登录账号、上面生成的加密密码EncodePsw、客户端生成的随机数和时间放入对应的位置当中去。需要注意在前后钱不要换行或者留有空格,否者服务端会将这个特殊字符当作参数进行计算,最后会与我们计算的值不一知道之验证失败
<?xml version="1.0" encoding="utf-8"?>
<s:Envelope
xmlns:s="http://www.w3.org/2003/05/soap-envelope">
<s:Header>
<Security s:mustUnderstand="1" xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<UsernameToken>
<Username>%s</Username>
<Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">%s</Password>
<Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">%s</Nonce>
<Created xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">%s</Created>
</UsernameToken>
</Security>
</s:Header>
<s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<SetUser xmlns="http://www.onvif.org/ver10/device/wsdl">
<User>
<Username xmlns="http://www.onvif.org/ver10/schema">%s</Username>
<Password xmlns="http://www.onvif.org/ver10/schema">%s</Password>
<UserLevel xmlns="http://www.onvif.org/ver10/schema">Administrator</UserLevel>
</User>
</SetUser>
</s:Body>
</s:Envelope>
向服务发送请求:
其中params就是上面的请求数据体
/**
* POST 请求
*/
public static String postRequest(String baseUrl, String params) throws Exception {
String receive = "";
// 新建一个URL对象
URL url = new URL(baseUrl);
// 打开一个HttpURLConnection连接
HttpURLConnection urlConn = (HttpURLConnection) url.openConnection();
//设置请求允许输入 默认是true
// Post请求必须设置允许输出 默认false
urlConn.setDoInput(true);
urlConn.setDoOutput(true);
// 设置为Post请求
urlConn.setRequestMethod("POST");
// Post请求不能使用缓存
urlConn.setUseCaches(false);
//设置本次连接是否自动处理重定向
urlConn.setInstanceFollowRedirects(true);
// 配置请求Content-Type,application/soap+xml
urlConn.setRequestProperty("Content-Type", "application/soap+xml");
// 开始连接
urlConn.connect();
// 发送请求数据
urlConn.getOutputStream().write(params.getBytes());
// 判断请求是否成功
if (urlConn.getResponseCode() == 200) {
// 获取返回的数据
InputStream is = urlConn.getInputStream();
byte[] data = new byte[1024];
int n;
while ((n = is.read(data)) != -1) {
receive = receive + new String(data, 0, n);
}
} else {
throw new Exception("ResponseCodeError : " + urlConn.getResponseCode());
}
// 关闭连接
urlConn.disconnect();
return receive;
}
向服务器请求修改onvif摄像头密码
hm¼\V~0Z:XEùE@å@@À¨ À¨ïÆPCß<=iµáPDþ
<s:Envelope
xmlns:s="http://www.w3.org/2003/05/soap-envelope">
<s:Header>
<Security s:mustUnderstand="1" xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<UsernameToken>
<Username>admin</Username>
<Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">1zkPnFZ1FrpjoER17Y4j1IbvqaU=</Password>
<Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">ySnRFg08OEejGk5jIz0WiacAAAAAAA==</Nonce>
<Created xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">2019-10-29T03:24:35.938Z
</Created>
</UsernameToken>
</Security>
</s:Header>
<s:Body
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<SetUser
xmlns="http://www.onvif.org/ver10/device/wsdl">
<User>
<Username
xmlns="http://www.onvif.org/ver10/schema">admin
</Username>
<Password
xmlns="http://www.onvif.org/ver10/schema">admin123
</Password>
<UserLevel
xmlns="http://www.onvif.org/ver10/schema">Administrator
</UserLevel>
</User>
</SetUser>
</s:Body>
</s:Envelope>
服务器返回
0Z:XEùhm¼\V~EíÒ
@@âÀ¨À¨ PïÆi»Cß@³P ú í"http://docs.oasis-open.org/wsrf/bf-2"
xmlns:wsntw="http://docs.oasis-open.org/wsn/bw-2"
xmlns:wsrf-rw="http://docs.oasis-open.org/wsrf/rw-2"
xmlns:wsaw="http://www.w3.org/2006/05/addressing/wsdl"
xmlns:wsrf-r="http://docs.oasis-open.org/wsrf/r-2"
xmlns:trc="http://www.onvif.org/ver10/recording/wsdl"
xmlns:tse="http://www.onvif.org/ver10/search/wsdl"
xmlns:trp="http://www.onvif.org/ver10/replay/wsdl"
xmlns:tnshik="http://www.hikvision.com/2011/event/topics"
xmlns:hikwsd="http://www.onvifext.com/onvif/ext/ver10/wsdl"
xmlns:hikxsd="http://www.onvifext.com/onvif/ext/ver10/schema"
xmlns:tas="http://www.onvif.org/ver10/advancedsecurity/wsdl">
<env:Body>
<tds:SetUserResponse/>
</env:Body>undefined</env:Envelope>
// 我写的
<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:tds="http://www.onvif.org/ver10/device/wsdl" xmlns:tt="http://www.onvif.org/ver10/schema">
<s:Header>
<Security xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" s:mustUnderstand="1">
<UsernameToken>
<Username>admin</Username>
<Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">CSguIFhe1wASn2Gol0HHhdOge34=</Password>
<Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">YBsdsEpDReymoouS8qHAFJOAm1wa6hD3rFX6VYPk8SYSwZrukU8tbBI22E4gDu</Nonce>
<Created xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">2019-10-30T16:09:34.828Z</Created>
</UsernameToken>
</Security>
</s:Header>
<s:Body>
<tds:SetUser>
<tds:User>
<tt:Username>admin</tt:Username>
<tt:Password>admin123</tt:Password>
<tt:UserLevel>Administrator</tt:UserLevel>
</tds:User>
</tds:SetUser>
</s:Body>
</s:Envelope>