手写海康OpenApi签名规范,实现手动调用api(sdk:artemis-http-client)

1. 前言:

artemis-http-client sdk 中提供获取门禁事件图片的方法,但实际图片访问地址为该响应的重定向地址
问题来了:虽然他提供了 sdk ,但没有办法通过 sdk 获取重定向的地址于是产生了本文,自己通过hutools的 httpUtil调用

2. 签名工具类

HikSignUtil 记得先引入一下 artemis-http-client

java 复制代码
package com.lxsy.util;

import cn.hutool.core.codec.Base64;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.crypto.digest.HMac;
import cn.hutool.crypto.digest.HmacAlgorithm;

import java.nio.charset.StandardCharsets;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;

/**
 * 海康 OpenAPI 签名工具类
 * 签名算法:HmacSHA256 + Base64
 */
public class HikSignUtil {

    private static final DateTimeFormatter RFC_1123_FORMATTER =
            DateTimeFormatter.RFC_1123_DATE_TIME.withZone(ZoneOffset.UTC);

    /**
     * 生成请求头(包含签名)
     *
     * @param appKey    应用Key
     * @param appSecret 应用Secret
     * @param url       请求的相对路径(如 /artemis/api/acs/v1/event/pictures)
     * @param body      请求体(JSON字符串)
     * @return 包含签名的请求头Map
     */
    public static Map<String, String> buildHeaders(String appKey, String appSecret, String url, String body) {
        // Content-MD5:请求体的MD5值,Base64编码
        String contentMd5 = Base64.encode(DigestUtil.md5(body));

        // Date 头(RFC1123)
        String date = RFC_1123_FORMATTER.format(ZonedDateTime.now(ZoneOffset.UTC));

        // 签名字符串拼接
        // httpHeaders = HTTP METHOD + "\n" + Accept + "\n" + Content-MD5 + "\n" + Content-Type + "\n" + Date + "\n"
        // customHeaders = "x-ca-key" + ":" + appKey + "\n"
        // signString = httpHeaders + customHeaders + url
        StringBuilder signBuilder = new StringBuilder();
        signBuilder.append("POST").append("\n");           // HTTP METHOD
        signBuilder.append("*/*").append("\n");            // Accept
        signBuilder.append(contentMd5).append("\n");       // Content-MD5
        signBuilder.append("application/json").append("\n"); // Content-Type
        signBuilder.append(date).append("\n");             // Date
        signBuilder.append("x-ca-key:").append(appKey).append("\n"); // customHeaders
        signBuilder.append(url);                           // url

        String signString = signBuilder.toString();

        // 使用 HmacSHA256 算法计算签名
        HMac hmac = new HMac(HmacAlgorithm.HmacSHA256, appSecret.getBytes(StandardCharsets.UTF_8));
        String signature = Base64.encode(hmac.digest(signString));

        // 构建请求头
        Map<String, String> headers = new HashMap<>();
        headers.put("Accept", "*/*");
        headers.put("Content-Type", "application/json");
        headers.put("Content-MD5", contentMd5);
        headers.put("Date", date);
        headers.put("x-ca-key", appKey);
        headers.put("x-ca-signature", signature);
        headers.put("x-ca-signature-headers", "x-ca-key");

        return headers;
    }
}

3. 使用

示例1

java 复制代码
protected static final ArtemisConfig artemisConfig = new ArtemisConfig("192.XX.13.XX:443", "123", "123");


    /**
     * 使用自建 HttpClient 调用海康接口,便于排查网络/签名问题
     */
    public static PageResponseResult<EventInfoDto> getEventListWithHttpClient(EventsRequest eventsRequest) throws Exception {
        if (eventsRequest == null) {
            eventsRequest = new EventsRequest();
        }

        String url = "/artemis/api/acs/v2/door/events";
        String fullUrl = "https://" + artemisConfig.getHost() + url;
        String body = JSON.toJSONString(eventsRequest);

        Map<String, String> headers = HikSignUtil.buildHeaders(
                artemisConfig.getAppKey(),
                artemisConfig.getAppSecret(),
                url,
                body
        );

        try (CloseableHttpClient httpClient = createHttpClient(false)) {
            HttpPost httpPost = new HttpPost(fullUrl);
            headers.forEach(httpPost::setHeader);
            httpPost.setEntity(new StringEntity(body, ContentType.APPLICATION_JSON));

            try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
                int status = response.getStatusLine().getStatusCode();
                String respBody = response.getEntity() == null ? null : EntityUtils.toString(response.getEntity());
                if (status == HttpStatus.SC_OK) {
                    return checkResp(respBody, new TypeReference<PageResponseResult<EventInfoDto>>() {
                    });
                }
                throw new Exception("获取事件列表失败,状态码: " + status + ", 响应: " + respBody);
            }
        }
    }

示例2

java 复制代码
protected static final ArtemisConfig artemisConfig = new ArtemisConfig("192.XX.13.XX:443", "123", "123");
    
public static String pictures(String svrIndexCode, String picUri) throws Exception {
        String url = "/artemis/api/acs/v1/event/pictures";
        String fullUrl = "https://" + artemisConfig.getHost() + url;

        // 构建请求体
        PicturesRequest request = new PicturesRequest(svrIndexCode, picUri);
        String body = JSON.toJSONString(request);

        // 使用签名工具生成请求头
        Map<String, String> headers = HikSignUtil.buildHeaders(
                artemisConfig.getAppKey(),
                artemisConfig.getAppSecret(),
                url,
                body
        );

        try (CloseableHttpClient httpClient = createHttpClient(false)) {
            HttpPost httpPost = new HttpPost(fullUrl);
            headers.forEach(httpPost::setHeader);
            httpPost.setEntity(new StringEntity(body, ContentType.APPLICATION_JSON));

            try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
                int status = response.getStatusLine().getStatusCode();
                if (status == HttpStatus.SC_MOVED_TEMPORARILY
                        || status == HttpStatus.SC_MOVED_PERMANENTLY
                        || status == HttpStatus.SC_SEE_OTHER
                        || status == HttpStatus.SC_TEMPORARY_REDIRECT
                        || status == HTTP_STATUS_PERMANENT_REDIRECT) {
                    Header locationHeader = response.getFirstHeader("Location");
                    if (locationHeader != null && StrUtil.isNotBlank(locationHeader.getValue())) {
                        return locationHeader.getValue();
                    }
                    throw new Exception("302重定向但未获取到Location头");
                }

                String respBody = response.getEntity() == null ? null : EntityUtils.toString(response.getEntity());
                throw new Exception("获取图片失败,状态码: " + status + ", 响应: " + respBody);
            }
        }
    }
相关推荐
默|笙25 分钟前
【Linux网络】传输层协议TCP(3)滑动窗口、流量控制
linux·网络·tcp/ip
xiaoye-duck1 小时前
《C++ 项目:手写大模型接入 SDK》ChatGPT接口设计(中):全量响应实现和测试
网络·c++·语言模型·chatgpt
无言听风雨丶2 小时前
systemd 与信号 / 僵尸进程:Linux 进程管理的底层逻辑与排错
linux·服务器·网络
小贾要学习3 小时前
【C++后端开发项目】Reactor 模型 / C++ 仿写 muduo HTTP 服务器:让底层网络库真正跑起网页
linux·服务器·c++·http·网络编程·muduo·reactor模型
g5zhu58963 小时前
没有公网ip的情况下,本地能ping服务器服务器不能ping本地,让服务器能访问本地某端口的简单方法
服务器·网络协议·tcp/ip
SDWAN_Cheap3 小时前
BGP是什么,广播IP是什么?两个容易混淆的概念拆解
网络·tcp/ip·bgp·广播ip
程序员JerrySUN3 小时前
PCIe Gen5 SSD 为什么读得快、写得慢?从存储原理到 Linux 性能实测
linux·服务器·网络
guizhoumen4 小时前
net core下WebSocket请求总是断开的解决方案
网络·websocket·网络协议
HKT_China4 小时前
香港电讯河套区创新科技中心暨AI机楼揭幕,以网络基建助力北部都会区发展
网络·ai
云边云科技_云网融合4 小时前
案例分享 | 某运动零售品牌多云治理实战:一套框架解决权限失控、网络混乱与成本黑箱
网络·零售