用户绑定
本文介绍如何实现 IM 用户与第三方应用用户的绑定。
数据表设计
用户绑定表 (im_bindings)
CREATE TABLE im_bindings (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL COMMENT '第三方应用用户ID',
tenant_id BIGINT NOT NULL COMMENT '第三方应用租户ID',
im_user_id VARCHAR(64) NOT NULL COMMENT 'IM用户ID',
im_workspace_id BIGINT NOT NULL COMMENT 'IM工作空间ID',
bind_status TINYINT DEFAULT 1 COMMENT '绑定状态: 1-已绑定, 0-已解绑',
binding_method VARCHAR(32) COMMENT '绑定方式: installer_auto/manual/oauth',
bind_time DATETIME COMMENT '绑定时间',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_user_tenant (user_id, tenant_id),
UNIQUE KEY uk_im_user_workspace (im_user_id, im_workspace_id)
) COMMENT '用户绑定表';
字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
user_id | BIGINT | 第三方应用用户 ID |
tenant_id | BIGINT | 第三方应用租户 ID |
im_user_id | VARCHAR | IM 用户 ID |
im_workspace_id | BIGINT | IM 工作空间 ID |
bind_status | TINYINT | 绑定状态:1-已绑定,0-已解绑 |
binding_method | VARCHAR | 绑定方式 |
bind_time | DATETIME | 绑定时间 |
唯一约束
uk_user_tenant- 确保一个用户在一个租户下只能有一个绑定uk_im_user_workspace- 确保一个 IM 用户在一个工作空间下只能绑定一个应用用户
绑定方式
| 绑定方式 | binding_method | 触发时机 | 说明 |
|---|---|---|---|
| 安装时自动绑定 | installer_auto | 应用安装时 | 安装者自动绑定,无需手动操作 |
| 手动绑定 | manual | 首次 SSO 登录时 | 其他用户需要手动登录并绑定 |
| OAuth 绑定 | oauth | OAuth 授权流程 | 通过标准 OAuth 流程绑定 |
后端实现示例
实体类
@Data
@TableName("im_bindings")
public class ImBinding implements Serializable {
@TableId(type = IdType.AUTO)
private Long id;
private Long userId;
private Long tenantId;
private String imUserId;
private Long imWorkspaceId;
private Integer bindStatus;
private String bindingMethod;
private LocalDateTime bindTime;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
Service 层
@Service
@Slf4j
public class ImBindingService {
@Autowired
private ImBindingMapper bindingMapper;
/**
* 创建安装者自动绑定
*/
public void createAutoBinding(Long userId, Long tenantId,
String imUserId, Long imWorkspaceId) {
// 检查是否已存在绑定
if (isImUserBound(imUserId, imWorkspaceId)) {
log.info("IM用户已绑定,跳过自动绑定: imUserId={}", imUserId);
return;
}
ImBinding binding = new ImBinding();
binding.setUserId(userId);
binding.setTenantId(tenantId);
binding.setImUserId(imUserId);
binding.setImWorkspaceId(imWorkspaceId);
binding.setBindStatus(1);
binding.setBindingMethod("installer_auto");
binding.setBindTime(LocalDateTime.now());
bindingMapper.insert(binding);
log.info("创建安装者自动绑定: userId={}, imUserId={}", userId, imUserId);
}
/**
* 创建手动绑定
*/
public void createManualBinding(Long userId, Long tenantId,
String imUserId, Long imWorkspaceId) {
// 检查是否已存在绑定
if (isImUserBound(imUserId, imWorkspaceId)) {
throw new ServiceException("该 IM 用户已绑定其他账号");
}
if (isUserBound(userId, tenantId)) {
throw new ServiceException("该账号已绑定其他 IM 用户");
}
ImBinding binding = new ImBinding();
binding.setUserId(userId);
binding.setTenantId(tenantId);
binding.setImUserId(imUserId);
binding.setImWorkspaceId(imWorkspaceId);
binding.setBindStatus(1);
binding.setBindingMethod("manual");
binding.setBindTime(LocalDateTime.now());
bindingMapper.insert(binding);
log.info("创建手动绑定: userId={}, imUserId={}", userId, imUserId);
}
/**
* 检查 IM 用户是否已绑定
*/
public boolean isImUserBound(String imUserId, Long imWorkspaceId) {
return bindingMapper.selectCount(
new LambdaQueryWrapper<ImBinding>()
.eq(ImBinding::getImUserId, imUserId)
.eq(ImBinding::getImWorkspaceId, imWorkspaceId)
.eq(ImBinding::getBindStatus, 1)
) > 0;
}
/**
* 检查应用用户是否已绑定
*/
public boolean isUserBound(Long userId, Long tenantId) {
return bindingMapper.selectCount(
new LambdaQueryWrapper<ImBinding>()
.eq(ImBinding::getUserId, userId)
.eq(ImBinding::getTenantId, tenantId)
.eq(ImBinding::getBindStatus, 1)
) > 0;
}
/**
* 根据 IM 用户获取绑定信息
*/
public ImBinding getByImUser(String imUserId, Long imWorkspaceId) {
return bindingMapper.selectOne(
new LambdaQueryWrapper<ImBinding>()
.eq(ImBinding::getImUserId, imUserId)
.eq(ImBinding::getImWorkspaceId, imWorkspaceId)
.eq(ImBinding::getBindStatus, 1)
);
}
/**
* 根据应用用户获取绑定信息
*/
public ImBinding getByUser(Long userId, Long tenantId) {
return bindingMapper.selectOne(
new LambdaQueryWrapper<ImBinding>()
.eq(ImBinding::getUserId, userId)
.eq(ImBinding::getTenantId, tenantId)
.eq(ImBinding::getBindStatus, 1)
);
}
/**
* 解除绑定
*/
public void unbind(Long userId, Long tenantId) {
bindingMapper.update(null,
new LambdaUpdateWrapper<ImBinding>()
.eq(ImBinding::getUserId, userId)
.eq(ImBinding::getTenantId, tenantId)
.set(ImBinding::getBindStatus, 0)
);
log.info("解除用户绑定: userId={}, tenantId={}", userId, tenantId);
}
}
安装者自动绑定
当用户在 IM 中发起应用安装时,系统会自动为安装者创建用户绑定。
触发条件
- 安装回调中包含
initiator_user_id参数 - 当前登录用户确认安装
绑定逻辑
@PostMapping("/api/im/install/confirm")
public Result confirmInstall(
@RequestBody InstallConfirmRequest request,
HttpSession session
) {
Long initiatorUserId = (Long) session.getAttribute("install_initiator_user_id");
Long workspaceId = (Long) session.getAttribute("install_workspace_id");
// ... 调用 IM 确认安装接口 ...
// 自动绑定安装者
if (initiatorUserId != null) {
User currentUser = getCurrentUser();
try {
imBindingService.createAutoBinding(
currentUser.getId(),
request.getTenantId(),
initiatorUserId.toString(),
workspaceId
);
} catch (Exception e) {
// 自动绑定失败不影响安装流程
log.warn("安装者自动绑定失败: {}", e.getMessage());
}
}
return Result.success("安装成功");
}
安全保护
- 不覆盖已有绑定 - 如果 IM 用户已绑定其他账号,不会覆盖
- 容错处理 - 自动绑定失败不影响安装流程
手动绑定(SSO 首次登录)
当用户通过 SSO 首次访问应用时,如果未绑定,需要手动绑定。
SSO 登录流程
绑定接口实现
@PostMapping("/api/im/binding/bind-with-login")
public Result bindWithLogin(@RequestBody BindWithLoginRequest request) {
// 1. 验证用户身份
User user = userService.login(request.getUsername(), request.getPassword());
// 2. 获取用户所属租户
Long tenantId = user.getTenantId();
// 3. 检查工作空间是否已绑定到该租户
WorkspaceBinding wsBinding = workspaceBindingService
.getByWorkspaceId(request.getImWorkspaceId());
if (wsBinding == null || !wsBinding.getOaTenantId().equals(tenantId)) {
return Result.error("该工作空间未绑定到您所属的租户");
}
// 4. 创建用户绑定
imBindingService.createManualBinding(
user.getId(),
tenantId,
request.getImUserId(),
request.getImWorkspaceId()
);
// 5. 生成登录态
String token = jwtService.generateToken(user);
return Result.success(Map.of(
"token", token,
"userId", user.getId()
));
}
查询绑定关系
根据 IM 用户查询
/**
* SSO 入口处理
*/
@GetMapping("/api/im/sso/entry")
public String handleSsoEntry(
@RequestParam("ticket") String ticket,
@RequestParam("workspace_id") Long workspaceId
) {
// 1. 验证 Ticket
TicketVerifyResponse ticketInfo = imApiClient.verifyTicket(ticket, workspaceId);
// 2. 查询用户绑定关系
ImBinding binding = imBindingService.getByImUser(
ticketInfo.getImUserId().toString(),
workspaceId
);
if (binding != null) {
// 已绑定,生成登录态并跳转到首页
User user = userService.getById(binding.getUserId());
String token = jwtService.generateToken(user);
return "redirect:/home?token=" + token;
} else {
// 未绑定,跳转到绑定页面
return "redirect:/im/bind?im_user_id=" + ticketInfo.getImUserId()
+ "&workspace_id=" + workspaceId;
}
}
根据应用用户查询
/**
* 向用户发送 IM 消息
*/
public void sendMessageToUser(Long userId, Long tenantId, String message) {
// 1. 查询用户绑定关系
ImBinding binding = imBindingService.getByUser(userId, tenantId);
if (binding == null) {
log.warn("用户未绑定 IM: userId={}", userId);
return;
}
// 2. 获取 Bot Token
String botToken = workspaceBindingService.getBotTokenByTenantId(tenantId);
// 3. 发送消息
imApiClient.sendMessage(
botToken,
binding.getImWorkspaceId(),
Long.parseLong(binding.getImUserId()),
message
);
}
绑定状态检查
检查用户是否已绑定
/**
* 检查当前用户是否已绑定 IM
*/
@GetMapping("/api/im/binding/status")
public Result getBindingStatus() {
User currentUser = getCurrentUser();
ImBinding binding = imBindingService.getByUser(
currentUser.getId(),
currentUser.getTenantId()
);
return Result.success(Map.of(
"bound", binding != null,
"imUserId", binding != null ? binding.getImUserId() : null,
"bindingMethod", binding != null ? binding.getBindingMethod() : null
));
}