Java 示例代码
本文档提供 Java 语言对接 IM 开放平台的完整示例代码,包括签名生成、API 调用和完整的对接示例。
环境要求
- Java 17+
- Spring Boot 3.x
- Maven 或 Gradle
依赖配置
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Hutool 工具库(用于签名计算) -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-crypto</artifactId>
<version>5.8.25</version>
</dependency>
<!-- OkHttp(HTTP 客户端) -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
<!-- Jackson(JSON 处理) -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Lombok(简化代码) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
配置类
应用配置
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "im")
@Data
public class ImProperties {
/**
* 应用 Client ID
*/
private String clientId;
/**
* 应用 Client Secret(仅服务端使用,禁止暴露到前端)
*/
private String clientSecret;
/**
* IM API 基础地址
*/
private String apiBaseUrl = "http://localhost:8088";
}
配置文件
# application.yml
im:
client-id: ${IM_CLIENT_ID}
client-secret: ${IM_CLIENT_SECRET}
api-base-url: ${IM_API_BASE_URL:http://localhost:8088}
安全提示
clientSecret 是敏感信息,请通过环境变量配置,禁止硬编码或提交到代码仓库。
签名工具类
签名是安装流程中验证请求合法性的关键机制。
签名公式
sign = SHA256(clientId + timestamp + clientSecret)
签名工具实现
import cn.hutool.crypto.SecureUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class ImSignUtil {
private final ImProperties imProperties;
/**
* 生成签名
*
* @param timestamp 时间戳(秒级,10位数字)
* @return 签名值(64位小写十六进制字符串)
*/
public String generateSign(Long timestamp) {
String signContent = imProperties.getClientId()
+ timestamp
+ imProperties.getClientSecret();
return SecureUtil.sha256(signContent);
}
/**
* 验证签名(用于验证 IM 回调)
*
* @param timestamp 时间戳
* @param sign 签名值
* @return 是否有效
*/
public boolean verifySign(Long timestamp, String sign) {
// 验证时间戳有效期(10分钟,允许1分钟时钟偏差)
long currentTimestamp = System.currentTimeMillis() / 1000;
long timeDiff = currentTimestamp - timestamp;
if (timeDiff > 600 || timeDiff < -60) {
return false;
}
// 验证签名
String calculatedSign = generateSign(timestamp);
return calculatedSign.equals(sign);
}
/**
* 获取当前时间戳(秒级)
*/
public Long getCurrentTimestamp() {
return System.currentTimeMillis() / 1000;
}
}
独立签名生成工具
用于测试和调试的独立工具类:
import cn.hutool.crypto.SecureUtil;
public class SignGenerator {
public static void main(String[] args) {
// 替换为您的应用凭证
String clientId = "550e8400-e29b-41d4-a716-446655440000";
String clientSecret = "7c9e6679-7425-40de-944b-e07fc1f90ae7";
Long timestamp = System.currentTimeMillis() / 1000;
// 生成签名
String signContent = clientId + timestamp + clientSecret;
String sign = SecureUtil.sha256(signContent);
System.out.println("=== 签名信息 ===");
System.out.println("clientId: " + clientId);
System.out.println("timestamp: " + timestamp);
System.out.println("sign: " + sign);
System.out.println("\n=== 测试 URL ===");
System.out.println("http://localhost:3001/api/im/install/callback"
+ "?workspace_id=1001"
+ "&workspace_name=测试工作空间"
+ "&app_id=123"
+ "&initiator_user_id=456"
+ "×tamp=" + timestamp
+ "&sign=" + sign);
}
}
API 客户端
封装 IM 开放平台 API 调用的客户端类。
响应类定义
import lombok.Data;
@Data
public class ApiResponse<T> {
private Integer code;
private String msg;
private T data;
public boolean isSuccess() {
return code != null && code == 200;
}
}
@Data
public class InstallConfirmResponse {
private String botToken;
private String tokenType;
private Long workspaceId;
private String workspaceName;
private Long botUserId;
private String botName;
private Long installationId;
private Long expiresIn;
private String scope;
}
@Data
public class TicketVerifyResponse {
private Long imUserId;
private Long imWorkspaceId;
private String imWorkspaceName;
private String imUserName;
private String imUserAvatar;
private String imUserPhone;
private String imUserPhoneFull;
}
@Data
public class BotMessageResponse {
private String messageId;
private Long channelId;
private Long roomId;
private String status;
private String errorMessage;
}
API 客户端实现
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Component
@RequiredArgsConstructor
@Slf4j
public class ImApiClient {
private final ImProperties imProperties;
private final OkHttpClient httpClient;
private final ObjectMapper objectMapper;
private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
/**
* 确认安装
*
* @param workspaceId IM 工作空间ID
* @param externalTenantId 第三方租户ID
* @param installerUserId 安装者用户ID
* @param timestamp 签名时间戳
* @param sign 签名值
* @return 安装确认响应(包含 Bot Token)
*/
public InstallConfirmResponse confirmInstall(
Long workspaceId,
String externalTenantId,
Long installerUserId,
Long timestamp,
String sign
) {
String url = imProperties.getApiBaseUrl()
+ "/im-user/api/v1/oauth/install/confirm";
Map<String, Object> body = new HashMap<>();
body.put("clientId", imProperties.getClientId());
body.put("workspaceId", workspaceId);
body.put("externalTenantId", externalTenantId);
body.put("installerUserId", installerUserId);
body.put("timestamp", timestamp);
body.put("sign", sign);
return post(url, body, null, InstallConfirmResponse.class);
}
/**
* 验证 Login Ticket
*
* @param botToken Bot Token
* @param ticket Login Ticket
* @return 用户信息
*/
public TicketVerifyResponse verifyTicket(String botToken, String ticket) {
String url = imProperties.getApiBaseUrl()
+ "/im-user/api/v1/oauth/ticket/verify";
Map<String, Object> body = new HashMap<>();
body.put("ticket", ticket);
return post(url, body, botToken, TicketVerifyResponse.class);
}
/**
* 发送消息给用户
*
* @param botToken Bot Token
* @param workspaceId 工作空间ID
* @param userId 目标用户ID
* @param text 消息文本
* @return 消息发送结果
*/
public BotMessageResponse sendMessage(
String botToken,
Long workspaceId,
Long userId,
String text
) {
return sendMessage(botToken, workspaceId, userId, text, null);
}
/**
* 发送消息给用户(带扩展字段)
*
* @param botToken Bot Token
* @param workspaceId 工作空间ID
* @param userId 目标用户ID
* @param text 消息文本
* @param payload 扩展字段(JSON 格式)
* @return 消息发送结果
*/
public BotMessageResponse sendMessage(
String botToken,
Long workspaceId,
Long userId,
String text,
String payload
) {
String url = imProperties.getApiBaseUrl()
+ "/im-user/api/v1/bot/send-message";
Map<String, Object> body = new HashMap<>();
body.put("workspaceId", workspaceId);
body.put("userId", userId);
body.put("text", text);
if (payload != null) {
body.put("payload", payload);
}
return post(url, body, botToken, BotMessageResponse.class);
}
/**
* 发送消息到频道
*
* @param botToken Bot Token
* @param channelId 频道ID
* @param msgText 消息文本
* @return 消息发送结果
*/
public BotMessageResponse sendChannelMessage(
String botToken,
Long channelId,
String msgText
) {
return sendChannelMessage(botToken, channelId, msgText, null);
}
/**
* 发送消息到频道(带扩展字段)
*
* @param botToken Bot Token
* @param channelId 频道ID
* @param msgText 消息文本
* @param payload 扩展字段
* @return 消息发送结果
*/
public BotMessageResponse sendChannelMessage(
String botToken,
Long channelId,
String msgText,
String payload
) {
// 注意:频道消息接口使用 Query 参数
String url = imProperties.getApiBaseUrl()
+ "/im-user/api/v1/bot/channels/" + channelId + "/messages"
+ "?msgText=" + urlEncode(msgText)
+ (payload != null ? "&payload=" + urlEncode(payload) : "");
return post(url, null, botToken, BotMessageResponse.class);
}
/**
* 通用 POST 请求
*/
private <T> T post(String url, Object body, String token, Class<T> responseType) {
try {
Request.Builder requestBuilder = new Request.Builder().url(url);
// 设置请求体
if (body != null) {
requestBuilder.post(RequestBody.create(
objectMapper.writeValueAsString(body), JSON
));
} else {
requestBuilder.post(RequestBody.create("", JSON));
}
// 设置 Authorization 头
if (token != null) {
requestBuilder.addHeader("Authorization", "Bearer " + token);
}
Response response = httpClient.newCall(requestBuilder.build()).execute();
String responseBody = response.body().string();
log.debug("API 响应: url={}, status={}, body={}", url, response.code(), responseBody);
if (!response.isSuccessful()) {
log.error("API 调用失败: url={}, status={}, body={}",
url, response.code(), responseBody);
throw new RuntimeException("API 调用失败: " + response.code());
}
ApiResponse<T> apiResponse = objectMapper.readValue(
responseBody,
objectMapper.getTypeFactory().constructParametricType(
ApiResponse.class, responseType
)
);
if (!apiResponse.isSuccess()) {
throw new RuntimeException(apiResponse.getMsg());
}
return apiResponse.getData();
} catch (IOException e) {
log.error("API 调用异常: url={}", url, e);
throw new RuntimeException("API 调用异常", e);
}
}
private String urlEncode(String value) {
try {
return java.net.URLEncoder.encode(value, "UTF-8");
} catch (Exception e) {
return value;
}
}
}
OkHttpClient 配置
import okhttp3.OkHttpClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
@Configuration
public class HttpClientConfig {
@Bean
public OkHttpClient okHttpClient() {
return new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build();
}
}
完整对接示例
安装回调处理
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/im")
@RequiredArgsConstructor
@Slf4j
public class ImInstallController {
private final ImApiClient imApiClient;
private final WorkspaceBindingService bindingService;
/**
* 安装回调接口(IM 跳转到此接口)
*/
@GetMapping("/install/callback")
public String handleInstallCallback(
@RequestParam("workspace_id") Long workspaceId,
@RequestParam("workspace_name") String workspaceName,
@RequestParam("app_id") Long appId,
@RequestParam(value = "initiator_user_id", required = false) Long initiatorUserId,
@RequestParam("timestamp") Long timestamp,
@RequestParam("sign") String sign,
HttpSession session
) {
log.info("收到安装回调: workspaceId={}, appId={}", workspaceId, appId);
// 保存安装信息到 Session
session.setAttribute("install_workspace_id", workspaceId);
session.setAttribute("install_workspace_name", workspaceName);
session.setAttribute("install_initiator_user_id", initiatorUserId);
session.setAttribute("install_timestamp", timestamp);
session.setAttribute("install_sign", sign);
// 重定向到前端安装确认页
return "redirect:/im/install-confirm";
}
/**
* 确认安装接口
*/
@PostMapping("/install/confirm")
public Result<Void> confirmInstall(
@RequestBody InstallConfirmRequest request,
HttpSession session
) {
// 1. 从 Session 获取安装信息
Long workspaceId = (Long) session.getAttribute("install_workspace_id");
Long initiatorUserId = (Long) session.getAttribute("install_initiator_user_id");
Long timestamp = (Long) session.getAttribute("install_timestamp");
String sign = (String) session.getAttribute("install_sign");
if (workspaceId == null || timestamp == null || sign == null) {
return Result.error("安装会话已过期,请重新安装");
}
// 2. 检查租户是否已被绑定
if (bindingService.isTenantBound(request.getTenantId())) {
return Result.error("该租户已绑定到其他工作空间");
}
// 3. 调用 IM 确认安装接口
InstallConfirmResponse response = imApiClient.confirmInstall(
workspaceId,
request.getTenantId().toString(),
initiatorUserId,
timestamp,
sign
);
// 4. 保存绑定关系
bindingService.createBinding(
request.getTenantId(),
workspaceId,
response.getBotToken(),
response.getExpiresIn()
);
// 5. 清理 Session
session.removeAttribute("install_workspace_id");
session.removeAttribute("install_workspace_name");
session.removeAttribute("install_initiator_user_id");
session.removeAttribute("install_timestamp");
session.removeAttribute("install_sign");
return Result.success();
}
}
SSO 免登处理
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/im/sso")
@RequiredArgsConstructor
@Slf4j
public class ImSsoController {
private final ImApiClient imApiClient;
private final WorkspaceBindingService bindingService;
private final ImBindingService imBindingService;
private final UserService userService;
/**
* SSO 入口接口(IM 跳转到此接口)
*/
@GetMapping("/entry")
public String handleSsoEntry(
@RequestParam("ticket") String ticket,
@RequestParam("workspace_id") Long workspaceId
) {
log.info("收到 SSO 请求: workspaceId={}", workspaceId);
// 1. 获取 Bot Token
WorkspaceBinding binding = bindingService.getByWorkspaceId(workspaceId);
if (binding == null) {
return "redirect:/error?message=应用未安装";
}
// 2. 验证 Ticket
TicketVerifyResponse verifyResponse = imApiClient.verifyTicket(
binding.getBotToken(),
ticket
);
// 3. 查询用户绑定
ImBinding imBinding = imBindingService.getByImUserId(
verifyResponse.getImUserId(),
workspaceId
);
if (imBinding != null) {
// 已绑定,生成登录态并跳转
User user = userService.getById(imBinding.getUserId());
String token = userService.generateToken(user);
return "redirect:/?token=" + token;
} else {
// 未绑定,跳转到绑定页面
return "redirect:/im/bind?im_user_id=" + verifyResponse.getImUserId()
+ "&workspace_id=" + workspaceId;
}
}
/**
* 绑定并登录接口
*/
@PostMapping("/bind-with-login")
public Result<LoginResponse> bindWithLogin(@RequestBody BindWithLoginRequest request) {
// 1. 验证用户身份
User user = userService.login(request.getUsername(), request.getPassword());
if (user == null) {
return Result.error("用户名或密码错误");
}
// 2. 检查是否已绑定其他 IM 用户
ImBinding existingBinding = imBindingService.getByUserId(
user.getId(),
user.getTenantId()
);
if (existingBinding != null) {
return Result.error("该账号已绑定其他 IM 用户");
}
// 3. 创建绑定关系
imBindingService.createBinding(
user.getId(),
user.getTenantId(),
request.getImUserId(),
request.getImWorkspaceId(),
"manual"
);
// 4. 生成登录态
String token = userService.generateToken(user);
return Result.success(new LoginResponse(token, user.getId()));
}
}
消息推送服务
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
@Slf4j
public class NotificationService {
private final ImApiClient imApiClient;
private final WorkspaceBindingService bindingService;
private final ImBindingService imBindingService;
/**
* 发送通知给用户
*
* @param userId 第三方应用用户ID
* @param tenantId 第三方应用租户ID
* @param message 消息内容
*/
public void notifyUser(Long userId, Long tenantId, String message) {
// 1. 查询用户绑定
ImBinding binding = imBindingService.getByUserId(userId, tenantId);
if (binding == null) {
log.warn("用户未绑定 IM: userId={}", userId);
return;
}
// 2. 获取 Bot Token
WorkspaceBinding wsBinding = bindingService.getByTenantId(tenantId);
if (wsBinding == null) {
log.warn("租户未安装应用: tenantId={}", tenantId);
return;
}
// 3. 发送消息
try {
BotMessageResponse response = imApiClient.sendMessage(
wsBinding.getBotToken(),
binding.getImWorkspaceId(),
binding.getImUserId(),
message
);
if ("sent".equals(response.getStatus())) {
log.info("消息发送成功: userId={}, messageId={}",
userId, response.getMessageId());
} else {
log.warn("消息发送失败: userId={}, error={}",
userId, response.getErrorMessage());
}
} catch (Exception e) {
log.error("消息发送异常: userId={}", userId, e);
}
}
/**
* 发送通知到频道
*
* @param tenantId 第三方应用租户ID
* @param channelId IM 频道ID
* @param message 消息内容
*/
public void notifyChannel(Long tenantId, Long channelId, String message) {
// 1. 获取 Bot Token
WorkspaceBinding wsBinding = bindingService.getByTenantId(tenantId);
if (wsBinding == null) {
log.warn("租户未安装应用: tenantId={}", tenantId);
return;
}
// 2. 发送频道消息
try {
BotMessageResponse response = imApiClient.sendChannelMessage(
wsBinding.getBotToken(),
channelId,
message
);
log.info("频道消息发送: channelId={}, status={}",
channelId, response.getStatus());
} catch (Exception e) {
log.error("频道消息发送异常: channelId={}", channelId, e);
}
}
}
curl 命令示例
确认安装
curl -X POST 'http://localhost:8088/im-user/api/v1/oauth/install/confirm' \
-H 'Content-Type: application/json' \
-d '{
"clientId": "550e8400-e29b-41d4-a716-446655440000",
"workspaceId": 1001,
"externalTenantId": "tenant_001",
"installerUserId": 456,
"timestamp": 1704528000,
"sign": "a1b2c3d4e5f6..."
}'
验证 Ticket
curl -X POST 'http://localhost:8088/im-user/api/v1/oauth/ticket/verify' \
-H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' \
-H 'Content-Type: application/json' \
-d '{
"ticket": "abc123def456..."
}'
发送消息
curl -X POST 'http://localhost:8088/im-user/api/v1/bot/send-message' \
-H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' \
-H 'Content-Type: application/json' \
-d '{
"workspaceId": 1001,
"userId": 123,
"text": "您有一条新的审批通知"
}'
发送频道消息
curl -X POST 'http://localhost:8088/im-user/api/v1/bot/channels/2001/messages?msgText=频道公告内容' \
-H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'