Appearance
安全管家OpenAPI文档V1.0
1.概述
本文档目的是方便第三方应用系统对接安全管家,包括但不限于将事件接入到安全管家或者从安全管家订阅事件
2.前置条件
向安全管家(简称IOC工作台,IOC)申请应用AK,SK,用来鉴定请求合法性。

2.1 对接前准备工作
找交付经理申请应用的AK,SK: 提供第三方系统名称
数据接入:第三方系统在边端,和安全管家在同一个局域网内,可通过HTTP或RabbitMQ方式接入;如果第三方应用在云端,则需要通过云端RabbitMQ方式接入,需要提供云端RabbitMQ信息
数据订阅: 如需从安全管家订阅数据,需要提供HTTP/HTTPS推送地址或者RabbitMq信息(IP,Port,VirtualHost,User,Password)
数据订阅以HTTP/HTTPS订阅的,第三方系统需要对推送请求鉴权,鉴权机制请查阅 OpenAPI通用鉴权机制
2.2 OpenAPI通用鉴权机制
请求接口时,携带三个请求头:Authorization、Date、 Content-Type
Authorization: 格式为 appkey:sign ,将appKey和sign签名用冒号连接, appkey为申请应用ak,sign为通过下文算法计算出来,详见sign签名算法
Date: 为请求时的时间,格式为:EEE, dd MMM yyyy HH:mm:ss zzz,如 Wed, 19 Nov 2025 02:41:20 GM
Content-Type:固定为 application/json
注:对于Content-Type的值,在请求头中传输的值可能后面会多一个" ;charset=utf-8"。第三方服务最好将Content-Type的值做截取操作,只取";"前的字符串,避免校验不通过。
2.2.1 sign签名算法
拼接字符串:将HTTP方法、Content-Type(固定为 application/json)、日期字符串和路径资源按以下格式拼接:
HTTP方法\nContent-Type\n日期字符串\n路径资源HmacSHA1加密:使用 appSecret 对拼接后的字符串进行HmacSHA1加密.
Base64编码:对加密结果进行Base64编码。
截取签名:从Base64编码结果中截取第5位开始的10个字符作为签名。
鉴权逻辑Java代码参考:
详见附录 附录三:鉴权相关参考代码
请求接口Java代码参考:
详见附录 附录三:鉴权相关参考代码
3.名称解释
事件:系统或服务中发生的状态变化,在变化超过阈值时,会主动出发通知, 要求立刻处理。本文中的事件也可被称为告警, 二者等同。
4.系统交互图介绍

安全管家会在事件状态发生变化时推送事件到订阅的第三方系统。状态的变化包括事件产生、事件由待办转为处理中、事件关闭。
5.接口文档
5.1 查询事件类型列表
接口定义和请求方式
接口地址: /api/open/queryIncidentTypeList
请求方式: POST
接口请求参数介绍:
| 字段 | 名称 | 数据类型 | 备注 |
|---|---|---|---|
| priority | 事件等级 | Integer | 非必填项,为空则查询全部 0:低风险 1:中风险 2:高风险 3:极高风险 |
| pageSize | 页大小 | Integer | 非必填项, 为空则使用默认值10 |
| pageNum | 页码 | Integer | 非必填项, 为空则使用默认值1 |
| scene | 场景 | Integer | 非必填项,为空则查询全部 1-报事事件 2-设备事件 3-AI事件 4-业务事件 5-消防事件 |
接口返回值介绍:
| 字段 | 名称 | 数据类型 | 备注 |
|---|---|---|---|
| code | 状态码 | String | 200:成功 |
| success | 是否成功 | Boolean | true:成功; false:失败; |
| data | 承载数据 | Object | |
| data中包含的对象信息 | |||
| pageNum | 当前页码 | Integer | 由入参决定 |
| pageSize | 当前页码数据量 | Integer | 由入参决定 |
| total | 数据量总数 | Integer | 事件的数据总量 |
| id | 事件类型id | String | 字符串长度为64个字符以下 |
| scene | 场景编码 | Integer | 1-报事事件, 2-设备事件, 3-AI事件, 4-业务事件, 5-消防事件 |
| sceneStr | 场景 | String | 场景编码对应的场景名称 |
| originParentCode | 原始父编码 | String | 注:字符串长度为128个字符以下 |
| originCode | 原始编码 | String | 注:字符串长度为128个字符以下 |
| parentName | 父名称 | String | 注:字符串长度为64个字符以下 |
| name | 名称 | String | 注:字符串长度为64个字符以下 |
| code | 唯一标识 | String | 注:字符串长度为128个字符以下 |
| status | 是否启用 | Integer | 1-是 0-否 |
| deleted | 是否删除 | Integer | 0-否,1-是 |
| priority | 事件等级标识 | Integer | 0:低风险 1:中风险 2:高风险 3:极高风险 |
| priorityStr | 事件等级 | String | 事件等级标识对应的名称 |
| createTime | 创建时间 | String | 使用"yyyy-MM-dd HH:mm:ss"格式 |
| updateTime | 更新时间 | String | 使用"yyyy-MM-dd HH:mm:ss"格式 |
| typeOriginCode | 一级事件类型code | String | 注:字符串长度为64个字符以下 |
| typeParentName | 一级事件类型name | String | 注:字符串长度为64个字符以下 |
请求示例:
请求参数:
(空)
响应报文:
json
{
"success": true,
"code": "200",
"data": {
"records": [
{
"id": "-7661345827197248065",
"scene": 3,
"sceneStr": "AI事件",
"originParentCode": "AISB_RYSD",
"originCode": "AISB_RYSD",
"parentName": "AI识别-人员摔倒",
"name": "AI识别-人员摔倒",
"code": "AISB_RYSD",
"status": 0,
"deleted": 0,
"priority": 2,
"priorityStr": "高风险",
"createTime": "2025-11-18 14:36:02",
"updateTime": "2025-11-18 15:23:09",
"typeOriginCode": "AISB_RYSD",
"typeParentName": "AI识别-人员摔倒"
}
],
"total": 79,
"pageSize": 10,
"pageNum": 1
}
}5.2 查询事件类型详情
接口定义和请求方式
接口地址: /api/open/queryIncidentTypeDetail
请求方式: POST
接口请求参数介绍
| 字段 | 名称 | 数据类型 | 备注 |
|---|---|---|---|
| secondTypeCode | 二级事件编码 | String | 必填 |
接口返回值介绍:
| 字段 | 名称 | 数据类型 | 备注 |
|---|---|---|---|
| code | 状态码 | String | 200:成功 |
| success | 是否成功 | Boolean | true:成功; false:失败 |
| data | 承载数据 | Object | 事件类型详情的查询结果 |
| data中包含的对象信息 | |||
| id | 事件类型id | String | 事件类型的id |
| scene | 场景编码 | Integer | 1-报事事件, 2-设备事件, 3-AI事件, 4-业务事件, 5-消防事件 |
| sceneStr | 场景 | String | 场景编码对应的场景名称 |
| originParentCode | 原始父编码 | String | 注:字符串长度大概为64个字符以下 |
| originCode | 原始编码 | String | 注:字符串长度大概为64个字符以下 |
| parentName | 父名称 | String | 注:字符串长度大概为64个字符以下 |
| name | 名称 | String | 注:字符串长度大概为64个字符以下 |
| code | 唯一标识 | String | 注:字符串长度大概为64个字符以下 |
| status | 是否启用 | Integer | 1-是 0-否 |
| deleted | 是否删除 | Integer | 0-否,1-是 |
| priority | 事件等级标识 | Integer | 0:低风险 1:中风险 2:高风险 3:极高风险 |
| priorityStr | 事件等级 | String | 事件等级标识对应的等级名称 |
| createTime | 创建时间 | String | 使用"yyyy-MM-dd HH:mm:ss"格式 |
| updateTime | 更新时间 | String | 使用"yyyy-MM-dd HH:mm:ss"格式 |
| voiceScene | 提示声音类型 | Integer | 事件在安全管家上报时,系统的提示音类型 |
| typeOriginCode | 一级事件类型code | String | 注:字符串长度大概为64个字符以下 |
| typeParentName | 一级事件类型name | String | 注:字符串长度大概为64个字符以下 |
返回报文:
json
{
"isOk": true,
"msg": null,
"cause": null,
"code": "200",
"data": {
"id": "-7661345827197248065",
"scene": 3,
"sceneStr": "AI事件",
"originParentCode": "AISB_RYSD",
"originCode": "AISB_RYSD",
"parentName": "AI识别-人员摔倒",
"name": "AI识别-人员摔倒",
"code": "AISB_RYSD",
"businessName": "AI识别-人员摔倒",
"businessKey": "AISB_RYSD",
"thresholdDuration": 0,
"thresholdTimes": 0,
"status": 0,
"autoReport": 0,
"autoReportStr": "否",
"aiCheck": 1,
"aiCheckStr": "是",
"dispatchType": "workOrder",
"autoDistributeTo": "workOrder",
"deleted": 0,
"priority": 2,
"priorityStr": "重要",
"createTime": "2025-11-18 14:36:02",
"updateTime": "2025-11-18 15:23:09",
"voiceScene": 1,
"typeOriginCode": "AISB_RYSD",
"typeParentName": "AI识别-人员摔倒",
"isWindow": 0
}
}5.3 分页查询事件列表
接口定义和请求方式
接口地址: /api/open/queryIncidentList
请求方式:POST
接口请求参数介绍:
| 字段 | 名称 | 数据类型 | 备注 |
|---|---|---|---|
| pageNum | 页码 | Integer | 非必填项,不填默认为1 需要查询第几页 |
| pageSize | 每页数据量 | Integer | 非必填项,不填默认为10 每页的数据量 |
| code | 事件编号 | String | 非必填项 字符串长度大概为64个字符以下 |
| typeId | 事件类型等级id | List<String> | 非必填项 0-低风险,1-中风险 2-高风险,3极高风险 |
| incidentType | 事件状态 | String | 非必填项 0:待处理 1:已结束 2:处理中 |
| startTime | 创建时间(开始) | String | 非必填项 定义一个时间作为事件结开始时间搜索的起点 格式:"yyyy-MM-dd HH:mm:ss" |
| endTime | 创建时间(结束) | String | 非必填项 定义一个时间作为事件结开始时间搜索的终点 格式:"yyyy-MM-dd HH:mm:ss" |
| eventStartTime | 关闭时间(开始) | String | 非必填项 定义一个时间作为事件结束时间搜索的起点 格式:"yyyy-MM-dd HH:mm:ss" |
| eventEndTime | 关闭时间(结束) | String | 非必填项 定义一个时间作为事件结束时间搜索的终点 格式:"yyyy-MM-dd HH:mm:ss" |
接口返回值介绍:
| 字段 | 名称 | 数据类型 | 备注 |
|---|---|---|---|
| code | 状态码 | String | 200为成功 |
| success | 是否成功 | Boolean | true:成功;false:失败 |
| data | 承载数据 | Object | 分页查询结果 |
| data中包含的对象信息 | |||
| pageNum | 当前页码 | Integer | 由入参决定 |
| pageSize | 当前页码数据量 | Integer | 由入参决定 |
| total | 数据量总数 | Integer | 事件的数据总量 |
| records | 数据详情 | List<IncidentDto> | 事件列表 |
| IncidentDto中包含的事件字段 | |||
| code | 事件编号 | String | 字符串长度在64个字符字符以下 |
| originIncidentCode | 第三方事件编号 | String | 事件源上报的事件编号,可追溯至相关系统产生的源事件 |
| name | 事件名称 | String | 对应事件的名称 |
| range | 项目 | String | 项目名称 |
| position | 位置 | String | 报事设备的位置 |
| typeKey | 事件类型 | String | 事件对应的二级事件编码 字符串长度在128个字符字符以下 |
| typeName | 事件类型名称 | String | 事件对应的二级事件编码名称 字符串长度在64个字符字符以下 |
| incidentType | 事件状态 | String | "0"--待处理; "1"--已结束; "2"--处理中; |
| createTime | 创建时间 | String | 事件上报至安全管家的时间 |
| eventEndTime | 关闭时间 | String | 事件的关闭时间 |
| alarmDuration | 持续时间 | String | 事件的持续时间(已换算成X天X小时X分X秒) |
请求示例:
请求参数:
json
{
"id": "",
"code": "",
"typeId": [
"1980462046801563649"
],
"incidentType": 1,
"queryType": 1,
"pageNum": 1,
"pageSize": 15,
"startTime": "2025-12-03 00:00:00",
"endTime": "2025-12-05 23:59:59",
"eventStartTime": "",
"eventEndTime": ""
}响应报文:
json
{
"success": true,
"code": "200",
"data": {
"records": [
{
"id": "1995301301597569024",
"code": "20251201AIYJ000095",
"originIncidentCode": "747173778632773",
"name": "AI识别-公共区域垃圾",
"range": "测试项目",
"position": "X座X单元XX区域",
"typeName": "AI识别-公共区域垃圾",
"typeKey": "AISB_GGQYLJ",
"createTime": "2025-12-01 09:17:56",
"eventEndTime": null,
"alarmDuration": "",
"incidentType": "0"
},
{
"id": "1995301121229914112",
"code": "20251201AIYJ000094",
"name": "AI识别-公共区域垃圾",
"range": "测试项目",
"typeName": "AI识别-公共区域垃圾",
"typeKey": "AISB_GGQYLJ",
"createTime": "2025-12-01 09:17:13",
"eventEndTime": null,
"alarmDuration": "",
"position": "X座X单元XX区域",
"incidentType": "0",
"originIncidentCode": "747173601284165"
}
],
"total": 7524,
"pageSize": 15,
"pageNum": 1,
"pages": 502
}
}5.4 查询事件详情
接口定义和请求方式:
接口地址:/api/open/queryIncidentDetail
请求方式: POST
接口请求参数介绍:
| 字段 | 名称 | 数据类型 | 备注 |
|---|---|---|---|
| id | 事件id | String | 必填项 事件的唯一id |
请求示例
请求参数:
json
{
"id": "1995297557426274304"
}接口返回值介绍:
| 字段 | 名称 | 数据类型 | 备注 |
|---|---|---|---|
| code | 状态码 | String | 200为成功 |
| success | 是否成功 | Boolean | true:成功;false:失败 |
| data | 承载数据 | Object | 分页查询结果 |
| incidentDetail | 事件详情信息 | Object | 包含在data中 存储事件的详情信息 |
| logOperationList | 调度记录 | List<LogOperationDto> | 包含在data中 存储该事件相关的调度和操作记录,如处理流程的派单,关闭等操作。 |
| records | 事件备注 | List<TempRecordModel> | 包含在data中 存储事件的一些备注信息。 |
incidentDetail包含字段
| 字段 | 名称 | 数据类型 | 备注 |
|---|---|---|---|
| id | 事件id | String | 事件的唯一id |
| name | 事件名称 | String | 字符串长度大概为64个字符以下 |
| description | 事件描述 | String | 对于事件的描述,通常为事件名称 |
| source | 事件来源 | String | 事件由哪个系统上报到安全管家 |
| deviceName | 报事设备名称 | String | 上报此事件的设备 |
| position | 位置 | String | 报事设备位置 |
| createTime | 上报时间 | String | 安全管家接收到事件的时间 |
| pictureUrl | 图片 | String | 此处为一个Json数组转化的字符串, 具体见报文示例中pictureUrl的格式 |
LogOperationDto包含字段
| 字段 | 名称 | 数据类型 | 备注 |
|---|---|---|---|
| eventId | 事件编号 | String | 字符串长度大概为64个字符以下 |
| workOrderNo | 工单编号 | String | 字符串长度大概为64个字符以下 |
| eventStatus | 流程状态 | String | 事件调度流程状态码 |
| eventName | 流程名称 | String | 事件调度流程状态文字描述 字符串长度大概为32个字符以下 |
| eventClosed | 工单是否关闭 | Boolean | true: 关闭 false: 未关闭 |
| eventHandler | 事件处理人 | String | 处理该事件的人员,如果为自动派单,该字段固定展示为:"AIOT告警自动派单" |
| eventHandlerMobile | 处理人手机号 | String | 处理人的手机号码,如果为自动派单,则此字段为null |
| eventTime | 处理时间 | String | 处理该事件的事件,格式为"yyyy-MM-dd HH:mm:ss" |
| eventType | 事件类型 | String | 此处为固定字符串"incident",表示为安全管家的事件。 |
| source | 来源 | String | 固定为字符串"system",代表调度记录由安全管家系统生成 |
| content | 节点描述 | String | 对于调度节点的描述信息 字符串长度不限。 |
| online | 是否在线 | String | 0:不在线; 1:在线. |
| reasonOption | AI事件关闭-原因选项 | String | 两个选项: 风险已解除; 识别错误; |
| reasonDescription | AI事件关闭-原因手动输入的描述 | String | 字符串长度为不超过256个字符 |
| robotId | 机器人id | String | 告警机器人或作业机器人的id 该字段只在派单类型为移动巡检时赋值 注:字符串长度为64个字符以下 |
| robotCameraId | 机器人绑定摄像头id | String | 机器人身上绑定摄像头的id 该字段只在派单类型为移动巡检时赋值 注:字符串长度为64个字符以下 |
TempRecordModel包含字段
| 字段 | 名称 | 数据类型 | 备注 |
|---|---|---|---|
| id | 事件id | String | 事件的唯一id |
| content | 内容 | String | 事件备注的内容 字符串长度不限 |
| operator | 操作人 | String | 撰写备注的人员 |
| phone | 手机号 | String | 操作人的手机号码 |
| createTime | 创建时间 | String | 备注的创建时间 |
响应报文:
json
{
"success": true,
"code": "200",
"data": {
"incidentDetail": {
"id": "1992847879056371712",
"code": "20251124AIYJ000004",
"name": "散落垃圾",
"typeId": "1980462046801563649",
"typeName": "散落垃圾",
"typeNameKey": "AIYJ_SLLJ",
"parentKey": "AIYJ",
"createTime": "2025-11-24 14:48:53",
"eventEndTime": null,
"alarmDuration": null,
"source": "集成服务",
"projectId": "P0000",
"projectName": "测试项目",
"startTime": "2025-11-24 14:48:53",
"incidentTime": "2025-11-24 14:27:21",
"position": "测试位置",
"alarmStatus": null,
"description": "散落垃圾",
"pictureUrl": "[\"https://spfile-sit.icloudcity.com/spioc/202511/5f3a4c33a24632e4e844/1c33052e4d30d03b.jpg\"]",
"deviceId": "7336649346697510912",
"deviceName": "测试设备",
"incidentType": "0",
"incidentCreateTime": "2025-11-24 14:48",
"mdmDeviceCode": "IOTPR03057100020",
"mdmDeviceName": "测试设备",
"originIncidentCode": "745153135341103",
"originParentCode": "AIYJ_SLLJ",
"dispatchType": "workOrder"
},
"logOperationList": [
{
"eventId": "1992847879056371712",
"workOrderNo": null,
"eventStatus": "00",
"eventName": "创建事件",
"eventClosed": false,
"eventHandler": "AIOT告警自动派单",
"eventTime": "2025-11-24 14:48",
"eventType": "incident",
"reasonOption": null,
"reasonDescription": null,
"robotId": "888620",
"robotCameraId": "888620"
}
],
"records": []
}
}6.事件订阅
6.1 增量事件订阅
实时推送事件,事件状态变化,都进行事件推送。包括待处理,进行中,关闭三种状态。
支持RabbitMq方式推送,需要应用方提供RabbitMq配置信息。
RabbitMQ方式订阅:
RabbitMQ的配置规则参考 附录二:RabbitMQ配置相关规则 。
推送事件报文格式介绍:
| 字段 | 名称 | 数据类型 | 备注 |
|---|---|---|---|
| objectType | 订阅类型 | String | 1-事件; 2-事件类型. 此处固定为1 |
| dataType | 数据类型 | String | 1-增量数据; 2-全量数据. |
| data | 数据详情 | List<Object> | 增量事件订阅时此处列表为1个元素,即安全管家新生成的事件 |
| List中包含的对象信息 | |||
| code | 事件编码 | String | 包含在Object中 注:字符串长度不超过64个字符 |
| name | 事件名称 | String | 注:字符串长度不超过32个字符 |
| originIncidentCode | 第三方事件编码 | String | 注:字符串长度不超过64个字符 |
| projectCode | 项目编码 | String | 注:字符串长度不超过32个字符 |
| projectName | 项目名称 | String | 注:字符串长度不超过32个字符 |
| position | 位置 | String | 报事位置信息 |
| pictureUrl | 事件图片 | String | 事件的图片url地址。 注:字符串长度不限,存储到数据库可使用LONGTEXT类型 |
| typeKey | 事件类型(二级事件编码) | String | 注:字符串长度不超过64个字符 |
| typeName | 事件类型名称(二级事件名称) | String | 注:字符串长度不超过32个字符 |
| incidentType | 事件状态 | String | 0:待处理 1:已完成 2:处理中 |
| createTime | 事件创建时间 | String | 事件推送到安全管家的时间 |
| eventEndTime | 事件关闭时间 | String | 事件在安全管家被关闭的时间 |
| alarmDuration | 事件持续时间 | String | 事件开始到结束共多久 已换算为X天X小时X分钟X秒 注意: 如果推送的事件不是已完成的状态, 此字段的值为空 |
| alarmDurationLong | 事件持续时间 | Long | 事件开始到结束共多久 (单位: 秒) 注意: 如果推送的事件不是已完成的状态, 此字段的值为空 |
| deviceId | 报事设备编码 | String | 注:字符串长度不超过64个字符 |
| deviceName | 报事设备名称 | String | 注:字符串长度不超过32个字符 |
| thirdCode | 第三方设备编码 | String | 注:字符串长度不超过64个字符 |
| thirdName | 第三方设备名称 | String | 注:字符串长度不超过64个字符 |
| workOrderCode | 工单编码 | String | 注:字符串长度不超过64个字符 |
推送报文示例:
json
{
"objectType": "1",
"dataType": "1",
"data": [{
"code": "20251106AIYJ000527",
"originIncidentCode": "738093083545669",
"name": "周界入侵",
"projectCode": "P0000",
"projectName": "测试项目",
"position": "测试位置",
"pictureUrl": "[\"https://spfile-uat.icloudcity.com/spioc/202511/18818ffed6116b378/1430a7687bbad2b98cd.jpg\"]",
"typeKey": "AIYJ_XQZJRQ",
"typeName": "周界入侵",
"incidentType": "1",
"createTime": "2025-11-06 10:11:00",
"eventEndTime": "2025-11-06 10:13:16",
"alarmDuration": "0天00小时2分钟16秒",
"alarmDurationLong": 136,
"deviceId": "IOTPR00001124200",
"deviceName": "A-TEST-ANGG-2001",
"thirdCode": "7321429522836889600",
"thirdName": "A-TEST-ANGG-2001"
}]
}7.附录
7.1 附录一:二级事件类型编码和名称对应关系
AI事件类型以及编码
| 二级事件类型名称 | 二级事件类型编码 |
|---|---|
| 周界入侵 | AIYJ_XQZJRQ |
| 机动车违停 | AIYJ_JDCWT |
| 车辆拥堵 | AIYJ_JDCDS |
| 消防通道占用 | AIYJ_XFTDDFZW |
| 人员拥堵 | AIYJ_CRKYD |
| 垃圾桶满溢 | AIYJ_LJTMY |
| 桶边垃圾 | AIYJ_TBLJ |
| 公共区域垃圾 | AIYJ_GGQYLJ |
| 员工离岗 | AIYJ_YGLG |
| 人员翻越 | AIYJ_RYFY |
| 室内明火检测 | AIYJ_SNMHJC |
| 烟雾检测 | AIYJ_YWJC |
| 专用车位占用 | AIYJ_ZYCWZY |
| 消防设备异常 | AIYJ_MHQQS |
| 抽烟识别 | AIYJ_CYSB |
| 人员摔倒 | AIYJ_RYSD |
| 人员逗留 | AIYJ_RYDL |
| 非机动车违停 | AIYJ_FJDCWT |
| 机动车滞留 | AIYJ_JDCZL |
| 玩手机识别 | AIYJ_WSJSB |
| 横幅识别 | AIYJ_HFSB |
| 散落垃圾 | AIYJ_SLLJ |
7.2 附录二:RabbitMQ配置相关规则
事件订阅和事件类型订阅:
RabbitMQ配置规则建议:
exchange类型:Topic类型
exchange命名规则:ioc_
routingkey命名规则:ioc_incident_out.$
queue命名规则建议:ioc_incident_queue_out_${appId}.${projectCode}
注:
对于queue的命名可根据实际情况是否需要拼接项目编码,由第三方系统自己创建,并绑定exchange。
7.3 附录三:鉴权相关参考代码
Java代码参考:
鉴权代码:
java
package com.onewo.openapi.auth;
import com.onewo.spsms.management.util.OkHttpUtil;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.util.StringUtils;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Base64;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
/**
* @author yangming
*/
@Slf4j
public class OpenApiClientAuth {
protected OpenApiClientAuth() {
}
public static final String AUTHORIZATION = "Authorization";
public static final String CONTENT_TYPE = "Content-Type";
public static final String DATE = "Date";
public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter
.ofPattern("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.ENGLISH);
/**
* @param httpMethod 请求方式 GET,POST,PUT。。。
* @param url http://baidu.com/xxx/xx?aa=11
* @param appKey 123123
* @param appSecret sdfdsf
* @param contentType 请求类型
* @return 返回授权说需要的Header参数
*/
@SneakyThrows
public static Map<String, String> getSignHeaders(final String httpMethod, final String url,
final String appKey, final String appSecret,
final String contentType) {
final String pathResource = OpenApiClientAuth.getPathResource(url);
final String contentTypeValue = contentType != null ? contentType : "application/json";
final String dateValue = DATE_TIME_FORMATTER.format(ZonedDateTime.now(ZoneId.of("GMT")));
final String sign = OpenApiClientAuth
.calculateSing(httpMethod, contentTypeValue, dateValue, pathResource,
appSecret);
final String authorizationString = appKey + ":" + sign;
final Map<String, String> authHeaders = new HashMap<>(4);
authHeaders.put(OpenApiClientAuth.DATE, dateValue);
authHeaders.put(OpenApiClientAuth.AUTHORIZATION, authorizationString);
authHeaders.put(OpenApiClientAuth.CONTENT_TYPE, contentTypeValue);
return authHeaders;
}
protected static String getPathResource(final String url) {
final String substring = url.substring(url.indexOf("://") + 3);
return substring.substring(substring.indexOf("/"));
}
protected static String calculateSing(final String httpMethodString,
final String contentTypeValue, final String dateValue, final String pathResource,
final String appSecret) throws InvalidKeyException, NoSuchAlgorithmException {
OpenApiClientAuth.log.debug("method = {}", httpMethodString);
OpenApiClientAuth.log.debug("contentTypeValue = {}", contentTypeValue);
OpenApiClientAuth.log.debug("dateValue = {}", dateValue);
OpenApiClientAuth.log.debug("pathResource = {}", pathResource);
OpenApiClientAuth.log.debug("appSecret = {}", appSecret);
final String signToString = OpenApiClientAuth
.builderStringToSign(httpMethodString, contentTypeValue, dateValue,
pathResource);
OpenApiClientAuth.log.debug("signToString = {}", signToString);
final String base64HashString = HMAC.hmacSha1Encrypt(signToString, appSecret);
OpenApiClientAuth.log.debug("base64HashString = {}", base64HashString);
final String sign = base64HashString.substring(5, 15);
OpenApiClientAuth.log.debug("sign = {}", sign);
return sign;
}
protected static String builderStringToSign(final String method,
final String contentTypeValue, final String dateValue, final String pathResource) {
return method
+ "\n"
+ OpenApiClientAuth.handleNullString(contentTypeValue)
+ "\n"
+ OpenApiClientAuth.handleNullString(dateValue)
+ "\n"
+ pathResource;
}
protected static String handleNullString(final String str) {
return !StringUtils.hasText(str) ? "" : str;
}
protected static class HMAC {
private HMAC() {
}
private static final String KEY_MAC_SHA1 = "HmacSHA1";
public static String hmacSha1Encrypt(final String encryptText, final String encryptKey)
throws NoSuchAlgorithmException, InvalidKeyException {
final byte[] text = encryptText.getBytes(StandardCharsets.UTF_8);
final byte[] keyData = encryptKey.getBytes(StandardCharsets.UTF_8);
final SecretKeySpec secretKey = new SecretKeySpec(keyData, HMAC.KEY_MAC_SHA1);
final Mac mac = Mac.getInstance(secretKey.getAlgorithm());
mac.init(secretKey);
return new String(Base64.getEncoder().encode(mac.doFinal(text)),
StandardCharsets.UTF_8);
}
}
/**
* get请求
*/
public static String openApiClientGet(String url, String body, String clientId, String clientSecret) {
Map<String, String> signHeaders = getSignHeaders(HttpMethod.GET.toString(), url, clientId, clientSecret, MediaType.APPLICATION_JSON_VALUE);
return OkHttpUtil.get(url, signHeaders, String.class);
}
/**
* post请求
*/
public static String openApiClientPost(String url, String body, String clientId, String clientSecret) {
Map<String, String> signHeaders = getSignHeaders(HttpMethod.POST.toString(), url, clientId, clientSecret, MediaType.APPLICATION_JSON_VALUE);
return OkHttpUtil.post(url, body, signHeaders, String.class);
}
}请求调用接口时鉴权Java代码参考:
java
public static void main(String[] args) {
String getUrl = "http://替换为实际的IP:30692/api/open/testGet?queryParam=GET参数";
String postUrl = "http://替换为实际的IP:30692/api/open/testPost";
String appkey = "替换为实际的appKey";
String appSecret = "替换为实际的appSecret";
// 无body示例
String result1 = OpenApiClientAuth.openApiClientGet(getUrl, null, appkey, appSecret);
log.info("OpenApi-test GET:{}",result1);
String result2 = OpenApiClientAuth.openApiClientPost(postUrl, "{\"name\":\"POST参数\"}", appkey, appSecret);
log.info("OpenApi-test POST:{}",result2);
}