跳到主要内容

SSO 集成指南

本文介绍如何实现用户从 IM 免登录访问您的应用。

实现原理​

SSO(单点登录)让用户在 IM 登录后,点击应用图标即可自动登录,无需再次输入密码。

IM 开放平台使用 Login Ticket 机制实现 SSO:

  1. 用户在 IM 中点击打开应用
  2. IM 生成一次性的 Login Ticket
  3. 跳转到第三方应用,携带 Ticket
  4. 第三方应用使用 Bot Token 验证 Ticket
  5. 验证成功后获取用户信息,完成登录

SSO 流程时序图​

建议跳转到第三方应用的后端接口,原因:

  1. 后端可以安全地使用 Bot Token 验证 Ticket
  2. 后端可以查询用户绑定关系
  3. 后端可以根据绑定状态决定重定向到哪个前端页面

Login Ticket 机制​

生成 Ticket​

IM 系统在用户点击打开应用时自动生成 Login Ticket。

生成 Ticket 接口: POST /im-user/api/v1/oauth/ticket

请求头: Authorization: Bearer {user_access_token}

请求参数:

{
"appId": 123,
"workspaceId": 1001
}

响应参数:

{
"code": 200,
"data": {
"ticket": "lt_550e8400e29b41d4a716446655440000",
"expiresIn": 300
}
}

Ticket 属性​

属性说明
ticket登录票据,以 lt_ 开头
expiresIn有效期(秒),默认 5 分钟

Ticket 特性​

  • 一次性使用 - 验证后立即失效
  • 短有效期 - 5 分钟内必须使用
  • 绑定用户 - 与生成时的用户绑定

验证 Ticket​

第三方应用收到 Ticket 后,需要调用 IM 接口验证。

验证 Ticket 接口: POST /im-user/api/v1/oauth/ticket/verify

请求头: Authorization: Bearer {bot_token}

请求参数:

{
"ticket": "lt_550e8400e29b41d4a716446655440000"
}

响应参数:

{
"code": 200,
"data": {
"imUserId": 123,
"imUserName": "张三",
"imUserPhone": "138****1234",
"imUserPhoneFull": "13812341234",
"imWorkspaceId": 1001,
"imWorkspaceName": "测试工作空间"
}
}

响应字段说明​

字段类型说明
imUserIdnumberIM 用户 ID
imUserNamestringIM 用户名称
imUserPhonestringIM 用户手机号(脱敏)
imUserPhoneFullstringIM 用户手机号(完整)
imWorkspaceIdnumberIM 工作空间 ID
imWorkspaceNamestringIM 工作空间名称

后端实现示例​

SSO 入口处理​

@GetMapping("/api/im/sso/entry")
public String handleSsoEntry(
@RequestParam("ticket") String ticket,
@RequestParam("workspace_id") Long workspaceId
) {
// 1. 获取 Bot Token
WorkspaceBinding binding = workspaceBindingService.getByWorkspaceId(workspaceId);
if (binding == null) {
return "redirect:/error?msg=workspace_not_bound";
}

String botToken = binding.getBotToken();

// 2. 验证 Ticket
TicketVerifyResponse ticketInfo;
try {
ticketInfo = imApiClient.verifyTicket(botToken, ticket);
} catch (Exception e) {
log.error("Ticket 验证失败: {}", e.getMessage());
return "redirect:/error?msg=ticket_invalid";
}

// 3. 查询用户绑定关系
ImBinding userBinding = imBindingService.getByImUser(
ticketInfo.getImUserId().toString(),
workspaceId
);

if (userBinding != null) {
// 4a. 已绑定,生成登录态并跳转到首页
User user = userService.getById(userBinding.getUserId());
String token = jwtService.generateToken(user);
return "redirect:" + frontendUrl + "/home?token=" + token;
} else {
// 4b. 未绑定,跳转到绑定页面
return "redirect:" + frontendUrl + "/im/bind?im_user_id="
+ ticketInfo.getImUserId() + "&workspace_id=" + workspaceId;
}
}

验证 Ticket 客户端​

@Service
public class ImApiClient {

private final String apiBaseUrl;

public TicketVerifyResponse verifyTicket(String botToken, String ticket) {
Map<String, Object> body = new HashMap<>();
body.put("ticket", ticket);

String response = HttpRequest.post(apiBaseUrl + "/im-user/api/v1/oauth/ticket/verify")
.header("Authorization", "Bearer " + botToken)
.header("Content-Type", "application/json")
.body(JSONUtil.toJsonStr(body))
.execute()
.body();

JSONObject json = JSONUtil.parseObj(response);
if (json.getInt("code") != 200) {
throw new ServiceException(json.getStr("message"));
}

return json.getBean("data", TicketVerifyResponse.class);
}
}

用户绑定接口​

@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()
));
}

安装者免绑定​

如果用户是应用安装者(在安装时已自动绑定),首次 SSO 登录时无需再次绑定,可以直接登录。

判断逻辑:

ImBinding userBinding = imBindingService.getByImUser(imUserId, workspaceId);

if (userBinding != null) {
// 已绑定(可能是 installer_auto 或 manual)
// 直接登录
}

错误处理​

错误场景处理方式
Ticket 无效或已过期提示用户重新从 IM 打开应用
工作空间未绑定提示用户先安装应用
Bot Token 无效提示管理员重新安装应用
用户未绑定跳转到绑定页面

下一步​