确认安装
确认应用安装,验证签名并返回 Bot Token。
接口说明
| 属性 | 值 |
|---|---|
| 请求方式 | POST |
| 请求路径 | /im-user/api/v1/oauth/install/confirm |
| 鉴权方式 | 签名验证 |
重要
此接口由第三方应用后端调用,用于确认安装并获取 Bot Token。请确保在服务端调用,不要在前端暴露 clientSecret。
请求头
Content-Type: application/json
请求参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
clientId | string | 是 | 应用 Client ID(UUID 格式) |
workspaceId | number | 是 | IM 工作空间 ID |
externalTenantId | string | 否 | 第三方应用租户 ID(用于建立绑定关系) |
installerUserId | number | 否 | 安装者 IM 用户 ID(用于自动绑定) |
state | string | 否 | 状态参数(需与发起安装时的 state 一致) |
timestamp | number | 是 | 签名时间戳(秒级,10位数字) |
sign | string | 是 | 签名值(64位小写十六进制字符串) |
请求示例
curl -X POST 'https://api.im.example.com/im-user/api/v1/oauth/install/confirm' \
-H 'Content-Type: application/json' \
-d '{
"clientId": "550e8400-e29b-41d4-a716-446655440000",
"workspaceId": 1001,
"externalTenantId": "1",
"installerUserId": 456,
"timestamp": 1736380800,
"sign": "3f8c2a1b4d5e6f7890abcdef1234567890abcdef1234567890abcdef12345678"
}'
响应参数
| 参数 | 类型 | 说明 |
|---|---|---|
botToken | string | Bot Token(用于调用 IM 开放 API) |
tokenType | string | Token 类型,固定为 "bot" |
workspaceId | number | 工作空间 ID |
workspaceName | string | 工作空间名称 |
botUserId | number | Bot 用户 ID |
botName | string | Bot 名称(与应用名称相同) |
installationId | number | 安装记录 ID |
expiresIn | number | Token 有效期(秒),默认 31536000(1年) |
scope | string | 权限范围 |
响应示例
成功响应
{
"code": 200,
"data": {
"botToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"tokenType": "bot",
"botUserId": 456,
"botName": "OA办公系统",
"workspaceId": 1001,
"workspaceName": "测试工作空间",
"installationId": 789,
"expiresIn": 31536000,
"scope": "bot:message:send,bot:user:read"
}
}
失败响应
{
"code": 400,
"msg": "签名验证失败"
}
签名验证
IM 系统会对请求进行 3 层验证:
1. 验证 Client ID
检查 clientId 是否存在且有效。
2. 验证时间戳
检查 timestamp 是否在有效期内:
- 有效期:10 分钟
- 允许时钟偏差:1 分钟
3. 验证签名
重新计算签名并对比:
calculatedSign = SHA256(clientId + timestamp + clientSecret)
签名必须完全匹配才能通过验证。
错误码
| 错误码 | 说明 | 解决方案 |
|---|---|---|
| 400 | clientId不能为空 | 确保传递了 clientId 参数 |
| 400 | workspaceId不能为空 | 确保传递了 workspaceId 参数 |
| 400 | timestamp不能为空 | 确保传递了 timestamp 参数 |
| 400 | sign不能为空 | 确保传递了 sign 参数 |
| 400 | 无效的 Client ID | 检查 clientId 是否正确 |
| 400 | 签名已过期 | 签名有效期为 10 分钟,请重新发起安装 |
| 400 | 签名验证失败 | 检查签名计算是否正确,确保 clientSecret 正确 |
Java 调用示例
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import java.util.HashMap;
import java.util.Map;
public class ImApiClient {
private final String apiBaseUrl;
private final String clientId;
private final String clientSecret;
public ImApiClient(String apiBaseUrl, String clientId, String clientSecret) {
this.apiBaseUrl = apiBaseUrl;
this.clientId = clientId;
this.clientSecret = clientSecret;
}
/**
* 确认应用安装
*
* @param workspaceId IM 工作空间 ID
* @param externalTenantId 第三方应用租户 ID
* @param installerUserId 安装者 IM 用户 ID
* @param timestamp 签名时间戳(从安装回调中获取)
* @param sign 签名值(从安装回调中获取)
* @return 安装确认响应
*/
public InstallConfirmResponse confirmInstall(
Long workspaceId,
String externalTenantId,
Long installerUserId,
Long timestamp,
String sign) {
Map<String, Object> body = new HashMap<>();
body.put("clientId", clientId);
body.put("workspaceId", workspaceId);
body.put("externalTenantId", externalTenantId);
body.put("installerUserId", installerUserId);
body.put("timestamp", timestamp);
body.put("sign", sign);
String response = HttpRequest.post(apiBaseUrl + "/im-user/api/v1/oauth/install/confirm")
.header("Content-Type", "application/json")
.body(JSONUtil.toJsonStr(body))
.execute()
.body();
JSONObject json = JSONUtil.parseObj(response);
if (json.getInt("code") != 200) {
throw new RuntimeException("确认安装失败: " + json.getStr("msg"));
}
return json.getBean("data", InstallConfirmResponse.class);
}
}
Python 调用示例
import hashlib
import requests
import time
class ImApiClient:
def __init__(self, api_base_url: str, client_id: str, client_secret: str):
self.api_base_url = api_base_url
self.client_id = client_id
self.client_secret = client_secret
def confirm_install(
self,
workspace_id: int,
external_tenant_id: str,
installer_user_id: int,
timestamp: int,
sign: str
) -> dict:
"""确认应用安装"""
url = f"{self.api_base_url}/im-user/api/v1/oauth/install/confirm"
payload = {
"clientId": self.client_id,
"workspaceId": workspace_id,
"externalTenantId": external_tenant_id,
"installerUserId": installer_user_id,
"timestamp": timestamp,
"sign": sign
}
response = requests.post(url, json=payload)
result = response.json()
if result.get("code") != 200:
raise Exception(f"确认安装失败: {result.get('msg')}")
return result.get("data")