1. 准备工作
(1)找大华ICC系统管理员申请访问凭证,一个clientId和一个clientSecret。
(2)除访问凭证外,还需要申请一个OpenAPI用户账号和密码。
(2)阅读ICC开放平台API,查询接口地址。
2. 开发
(1)引入大华ICC的鉴权SDK
大华ICC平台利用oauth实现第三方身份认证及接口鉴权。根据ICC开放平台文档,我们可以自行构建HTTP请求进行身份认证,也可以直接利用大华提供的SDK开发工具包进行登录认证操作。
在pom.xml文件中引入java-sdk-oauth
包:
<dependency>
<groupId>com.dahuatech.icc</groupId>
<artifactId>java-sdk-oauth</artifactId>
<version>1.0.9</version>
</dependency>
(2)API接口和相关模型类
public class DahuaAPI {
public static final String GET_PUBLIC_KEY = "/evo-apigw/evo-oauth/1.0.0/oauth/public-key";
public static final String USER_LOGIN = "/evo-apigw/evo-oauth/1.0.0/oauth/extend/token";
public static final String REFRESH_TOKEN = "/evo-apigw/evo-oauth/1.0.0/oauth/extend/refresh/token";
public static final String CAPTURE_PIC = "/evo-apigw/admin/API/EVO/invoke/DMS";
public static final String OSS = "/evo-apigw/evo-oss/";
}
ICC平台令牌包装类
@Data
public class Token {
private String token;
private String refreshToken;
private long getTokenTime;
private long expireTime;
private String tokenType;
public Token(IccTokenResponse.IccToken iccToken){
this.token = iccToken.getAccess_token();
this.refreshToken = iccToken.getRefresh_token();
this.getTokenTime = System.currentTimeMillis();
// 把过期时间设置成真实过期时间 - 10分钟
this.expireTime = this.getTokenTime + iccToken.getExpires_in() * 1000;
this.expireTime = this.expireTime - 60 * 10 * 1000;
this.tokenType = iccToken.getToken_type();
}
}
传递大华相关配置的模型类
@Data
public class DahuaConfig {
private String serverIp;
/**
* 大华ICC平台提供一个HTTP端口和一个HTTPS端口
*/
private String httpPort;
private String httpsPort;
private String clientId;
private String clientSecret;
private String username;
private String password;
}
(3)身份认证
新建AuthService.java
public class AuthService {
/**
* 登录
*/
public Token doLogin(DahuaConfig config) throws ClientException {
String url = "https://" + config.getServerIp() + ":" + config.getHttpsPort() + DahuaAPI.USER_LOGIN;
String publicKey = this.getPublicKey(config);
Map<String, Object> map = new HashMap();
// 使用密码模式进行认证
map.put("grant_type", "password");
map.put("username", config.getUsername());
// 用户密码需要加密
map.put("password", SignUtil.encryptRSA(config.getPassword(), publicKey));
map.put("client_id", config.getClientId());
map.put("client_secret", config.getClientSecret());
map.put("public_key", publicKey);
IccHttpHttpRequest pr = new IccHttpHttpRequest(url, Method.POST, JSONUtil.toJsonStr(map));
String prBody = pr.execute();
IccTokenResponse keyResp = BeanUtil.toBean(prBody, IccTokenResponse.class);
if (keyResp.isSuccess()) {
IccTokenResponse.IccToken iccToken = keyResp.getData();
return new Token(iccToken);
}
throw new RuntimeException(keyResp.getErrMsg());
}
/**
* 刷新令牌
*/
public Token refreshToken(Token token, DahuaConfig config) throws ClientException {
String url = "https://" + config.getServerIp() + ":" + config.getHttpsPort() + DahuaAPI.REFRESH_TOKEN;
log.info("refreshing token");
Map<String, Object> map = new HashMap();
map.put("grant_type", GrantType.refresh_token.name());
map.put("client_id", config.getClientId());
map.put("client_secret", config.getClientSecret());
map.put("refresh_token", token.getRefreshToken());
IccHttpHttpRequest pr = new IccHttpHttpRequest(url, Method.POST, JSONUtil.toJsonStr(map));
String prBody = pr.execute();
IccTokenResponse keyResp = BeanUtil.toBean(prBody, IccTokenResponse.class);
if (keyResp.isSuccess()) {
IccTokenResponse.IccToken iccToken = keyResp.getData();
return new Token(iccToken, config);
}
throw new RuntimeException(keyResp.getErrMsg());
}
/**
* 登录前需要获取publicKey
*/
private String getPublicKey(DahuaConfig config) throws ClientException {
String url = "https://" + config.getServerIp() + ":" + config.getHttpsPort() + DahuaAPI.GET_PUBLIC_KEY;
IccHttpHttpRequest pubRequest = new IccHttpHttpRequest(url, Method.GET);
log.info("getting public key");
String pubBody = pubRequest.execute();
OauthPublicKeyResponse keyResp = BeanUtil.toBean(pubBody, OauthPublicKeyResponse.class);
if (keyResp.isSuccess()) {
return keyResp.getData().getPublicKey();
}
throw new RuntimeException(keyResp.getErrMsg());
}
}
在AuthServer.java类中,提供了一个登录方法,同时提供了一个刷新令牌方法。
这里需要小伙伴们自己写一个TokenManager,将登录获取到的token缓存起来,避免每次使用时都申请新令牌。同时定期查看缓存令牌的有效期,过期前调用refreshToken方法获取新令牌。
(4)抓图
新建PictureService.java
我的例子中将取到的图片转成了base64,直接用了。如果需要保存到本地,调用IO写一下就好了。
@Slf4j
public class PictureService {
private ObjectMapper objectMapper = new ObjectMapper();
/**
* 手动抓图
*/
public String capturePicture(String pointCode, DahuaConfig config, Token token) {
if (token == null) {
throw new RuntimeException("未取得令牌");
}
String imgBase64 = null;
try {
String url = "http://" + dahuaConfig.getServerIp() + ":" + dahuaConfig.getHttpPort() + DahuaBaseAPI.CAPTURE_PIC;
String[] pointCodeDetails = pointCode.split("\\$");
// pointCode前面一部分作为设备位号
String devicePointCode = pointCodeDetails[0];
// pointCode最后部分作为通道号
String devChannel = pointCodeDetails[pointCodeDetails.length - 1];
Map<String, Object> deviceMap = new HashMap<>();
deviceMap.put("CmdSrc", 0);
deviceMap.put("DevChannel", devChannel);
deviceMap.put("SnapType", 1);
deviceMap.put("DevID", devicePointCode);
Map<String, Object> operationMap = new HashMap<>();
operationMap.put("method", "dev.snap");
operationMap.put("id", System.currentTimeMillis());
operationMap.put("params", deviceMap);
String operationJson = objectMapper.writeValueAsString(operationMap);
Map<String, String> uriVariables = new LinkedHashMap<>(3);
uriVariables.put("deviceCode", devicePointCode);
uriVariables.put("operation", "generalJsonTransport");
uriVariables.put("params", operationJson);
String uriVariablesJson = objectMapper.writeValueAsString(uriVariables);
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", token.getTokenType() + " " + token.getToken());
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> httpEntity = new HttpEntity<>(uriVariablesJson, headers);
ResponseEntity<Map> responseEntity = new RestTemplate().exchange(url, HttpMethod.POST, httpEntity, Map.class);
Map<String, Object> body = responseEntity.getBody();
Object code = body.get("code");
Object desc = body.get("desc");
Object data = body.get("data");
// 抓图成功时,服务器会返回图片地址
if (code != null && code.toString().equals("1000")) {
data = data.toString().replaceAll("\n", "").replaceAll(" ", "");
Map<String, Object> dataMap = objectMapper.readValue(data.toString(), Map.class);
Map<String, Object> picInfo = (Map<String, Object>) dataMap.get("params");
String picPath = (String) picInfo.get("PicInfo");
// OSS图像地址拼接
picPath = "http://" + dahuaConfig.getServerIp() + ":" + dahuaConfig.getHttpPort() + DahuaAPI.OSS + picPath + "?token=" + token.getToken();
imgBase64 = this.downloadPic(picPath);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return imgBase64;
}
public String downloadPic(String picUrl) {
URL url = null;
try {
url = new URL(picUrl);
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
byte[] buf = new byte[4096];
HttpURLConnection conn = null;
InputStream inStream = null;
String imgBase64 = null;
try {
//构造连接
conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
//这个网站要模拟浏览器才行
conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; rv:11.0) like Gecko");
//打开连接
conn.connect();
//打开这个网站的输入流
inStream = conn.getInputStream();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
int len = 0;
while ((len = inStream.read(buf)) != -1) {
byteArrayOutputStream.write(buf, 0, len);
}
byteArrayOutputStream.flush();
byte[] imgContent = byteArrayOutputStream.toByteArray();
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(imgContent);
BufferedImage image = ImageIO.read(byteArrayInputStream);
if (image != null) {
imgBase64 = Base64Utils.imageToBase64ByLocalByte(imgContent);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
} finally {
if (inStream != null) {
try {
inStream.close();
} catch (IOException e) {
}
}
}
return imgBase64;
}
}
3. 说明
(1)都是忙里偷闲写的这些东西,写的过程中删除了小部分与实际项目相关的代码。如果copy出去发现编译不过,留个言研究研究。
(2)大华ICC平台不支持高频抓图,高频抓图时返回的图片会与传入的设备位号不同。如,对100个设备同时抓图,可能抓1000001设备时,返回了1000009设备的图片。这个问题也没什么好办法去解决,我们直接用限流工具控制抓图速度了。