SSO 集成指南
本文介绍如何实现用户从 IM 免登录访问您的应用。
实现原理
SSO(单点登录)让用户在 IM 登录后,点击应用图标即可自动登录,无需再次输入密码。
IM 开放平台使用 Login Ticket 机制实现 SSO:
- 用户在 IM 中点击打开应用
- IM 生成一次性的 Login Ticket
- 跳转到第三方应用,携带 Ticket
- 第三方应用使用 Bot Token 验证 Ticket
- 验证成功后获取用户信息,完成登录
SSO 流程时序图
建议跳转到第三方应用的后端接口,原因:
- 后端可以安全地使用 Bot Token 验证 Ticket
- 后端可以查询用户绑定关系
- 后端可以根据绑定状态决定重定向到哪个前端页面
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": "测试工作空间"
}
}
响应字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
imUserId | number | IM 用户 ID |
imUserName | string | IM 用户名称 |
imUserPhone | string | IM 用户手机号(脱敏) |
imUserPhoneFull | string | IM 用户手机号(完整) |
imWorkspaceId | number | IM 工作空间 ID |
imWorkspaceName | string | IM 工作空间名称 |
后端实现示例
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 无效 | 提示管理员重新安装应用 |
| 用户未绑定 | 跳转到绑定页面 |