OpenIM 源码深度解析系列(二):双Token认证机制与接入流程

1,261 阅读37分钟

双Token认证机制与接入流程

1. 系统架构概览

OpenIM权限验证机制采用分层架构设计,主要包含以下组件:

  • Chat系统:负责用户注册、登录认证、API Token管理
  • OpenIM Server:负责IM功能、WebSocket连接认证、用户Token管理
  • openim-sdk-core:客户端SDK,负责与服务端的通信和Token管理
  • 设备端应用:Android/iOS等客户端应用

涉及存储结构如下:

graph TB
    subgraph "应用层"
        ChatAPI[Chat API]
        OpenIMAPI[OpenIM API]
        WebSocket[WebSocket Gateway]
        SDK[OpenIM SDK]
    end

    subgraph "Redis缓存层"
        subgraph "Chat Token缓存"
            ChatTokenCache["CHAT_UID_TOKEN_STATUS:{userID}<br/>Hash存储多Token状态<br/>• token1: 0 (正常)<br/>• token2: 1 (无效)<br/>• token3: 2 (被踢)<br/>• token4: 3 (过期)"]
        end
        
        subgraph "OpenIM Admin Token缓存"
            AdminTokenCache["CHAT:IM_TOKEN:{adminUserID}<br/>String存储<br/>TTL: 4分钟<br/>管理员系统调用凭证"]
        end
        
        subgraph "OpenIM User Token缓存"
            UserTokenCache["UID_PID_TOKEN_STATUS:{userID}:{platform}<br/>Hash存储平台Token<br/>• token1: 1 (正常)<br/>• token2: 2 (被踢)"]
        end
        
        subgraph "验证码缓存"
            VerifyCache["验证码临时缓存<br/>TTL: 300秒<br/>account:code 键值对"]
        end
    end

    subgraph "MongoDB持久化层"
        subgraph "Chat数据库"
            RegistersDB[("registers集合<br/>用户注册记录<br/>• user_id (主键)<br/>• device_id<br/>• ip<br/>• platform<br/>• account_type")]
            
            AccountsDB[("accounts集合<br/>用户账户密码<br/>• user_id (主键)<br/>• password (MD5)<br/>• create_time")]
            
            AttributesDB[("attributes集合<br/>用户详细属性<br/>• user_id (主键)<br/>• phone_number<br/>• email<br/>• nickname")]
            
            CredentialsDB[("credentials集合<br/>登录凭证管理<br/>• user_id<br/>• account<br/>• type (手机/邮箱)")]
            
            VerifyCodesDB[("verify_codes集合<br/>验证码记录<br/>• _id (主键)<br/>• account<br/>• code<br/>• used")]
            
            LoginRecordsDB[("user_login_records集合<br/>登录历史记录<br/>• user_id<br/>• login_time<br/>• ip<br/>• platform")]
        end
        
        subgraph "OpenIM数据库"
            UsersDB[("users集合<br/>IM用户基本信息<br/>• user_id (主键)<br/>• nickname<br/>• face_url<br/>• app_manger_level")]
        end
    end

    %% 数据流连接
    ChatAPI --> ChatTokenCache
    ChatAPI --> RegistersDB
    ChatAPI --> AccountsDB
    ChatAPI --> AttributesDB
    
    OpenIMAPI --> AdminTokenCache
    OpenIMAPI --> UserTokenCache
    OpenIMAPI --> UsersDB
    
    WebSocket --> UserTokenCache
    SDK --> UserTokenCache
    
    ChatTokenCache -.-> AccountsDB
    AdminTokenCache -.-> UsersDB
    UserTokenCache -.-> UsersDB
    
    VerifyCache -.-> VerifyCodesDB
    
    style ChatTokenCache fill:#e1f5fe
    style AdminTokenCache fill:#f3e5f5
    style UserTokenCache fill:#e8f5e8
    style VerifyCache fill:#fff3e0

2. 核心数据库表结构

2.1 Chat系统数据库表

2.1.1 registers - 用户注册记录表

表作用: 记录用户注册时的详细信息,用于统计分析和安全追踪。

集合名称: registers

字段名类型是否必需描述
user_idstring用户ID,业务主键
device_idstring设备ID
ipstring注册IP地址
platformstring注册平台:iOS、Android、Web等
account_typestring账户类型:phone、email、account等
modestring注册模式
create_timetime.Time注册时间

索引设计:

db.registers.createIndex({"user_id": 1}, {unique: true})
db.registers.createIndex({"ip": 1})
2.1.2 accounts - 用户账户表

表作用: 存储用户的登录账户和密码信息,用于身份验证。

集合名称: accounts

字段名类型是否必需描述
user_idstring用户ID,业务主键
passwordstring用户密码(MD5加密存储)
create_timetime.Time账户创建时间
change_timetime.Time密码最后修改时间
operator_user_idstring操作者用户ID

索引设计:

db.accounts.createIndex({"user_id": 1}, {unique: true})
2.1.3 attributes - 用户属性表

表作用: 存储用户的详细属性信息,包括个人信息、偏好设置、权限控制等。

集合名称: attributes

字段名类型是否必需描述
user_idstring用户ID,业务主键
accountstring账号名称
phone_numberstring手机号码
area_codestring区号
emailstring邮箱地址
nicknamestring用户昵称
face_urlstring头像URL
genderint32性别:1=男,2=女,0=未知
create_timetime.Time创建时间
change_timetime.Time最后修改时间
birth_timetime.Time出生日期
levelint32用户等级
allow_vibrationint32是否允许震动:0=不允许,1=允许
allow_beepint32是否允许提示音:0=不允许,1=允许
allow_add_friendint32是否允许被添加好友:0=不允许,1=允许
global_recv_msg_optint32全局接收消息选项:0=正常接收,1=不接收,2=仅在线接收
register_typeint32注册类型:1=手机号,2=邮箱,3=账号

索引设计:

db.attributes.createIndex({"user_id": 1}, {unique: true})
db.attributes.createIndex({"phone_number": 1})
db.attributes.createIndex({"email": 1})
2.1.4 credentials - 用户凭证表

表作用: 管理用户的登录凭证信息,支持多种登录方式(手机号、邮箱等)。

集合名称: credentials

字段名类型是否必需描述
user_idstring用户ID
accountstring登录账号(手机号或邮箱)
typeint凭证类型:1=手机号,2=邮箱,3=账户名
allow_changebool是否允许修改
create_timetime.Time创建时间

索引设计:

db.credentials.createIndex({"user_id": 1, "type": 1}, {unique: true})
db.credentials.createIndex({"account": 1}, {unique: true})
2.1.5 verify_codes - 验证码表

表作用: 存储各种验证码信息,用于用户注册、登录、修改密码等操作的安全验证。

集合名称: verify_codes

字段名类型是否必需描述
_idstring验证码ID,业务主键
accountstring关联的账号(手机号或邮箱)
platformstring平台标识
codestring验证码内容
durationuint有效期(秒)
countint使用次数
usedbool是否已使用
create_timetime.Time创建时间

索引设计:

db.verify_codes.createIndex({"_id": 1}, {unique: true})
db.verify_codes.createIndex({"account": 1})
db.verify_codes.createIndex({"create_time": 1}, {expireAfterSeconds: 300})
2.1.6 user_login_records - 用户登录记录表

表作用: 记录用户每次登录的详细信息,用于安全监控和行为分析。

集合名称: user_login_records

字段名类型是否必需描述
user_idstring用户ID
login_timetime.Time登录时间
ipstring登录IP地址
device_idstring设备ID
platformstring登录平台:iOS、Android、Web等

索引设计:

db.user_login_records.createIndex({"user_id": 1})
db.user_login_records.createIndex({"login_time": 1})

2.2 OpenIM Server数据库表

2.2.1 users - 用户基本信息表

表作用: 存储用户的基本信息,包括昵称、头像、管理权限、全局消息接收设置等核心数据。

集合名称: users

字段名类型是否必需描述
user_idstring用户唯一标识符,业务主键
nicknamestring用户昵称,显示名称
face_urlstring用户头像URL地址
exstring扩展字段,JSON格式存储自定义信息
app_manger_levelint32应用管理员级别:0=普通用户,1=管理员,2=超级管理员
global_recv_msg_optint32全局接收消息选项:0=正常接收,1=不接收,2=仅在线接收
create_timetime.Time账户创建时间

索引设计:

db.users.createIndex({"user_id": 1}, {unique: true})

3. Token系统详解

OpenIM系统实现了三种不同类型的Token,每种Token都有其特定的用途和验证机制。

3.1 Chat Token - Chat系统API认证

3.1.1 类型和用途
  • 用途:Chat系统REST API认证,用于访问用户管理、配置管理等功能
  • 生成者:Chat Admin RPC服务
  • 验证者:Chat系统中间件
  • 适用场景:管理后台、Chat API调用
3.1.2 Token结构
// chat/pkg/common/tokenverify/token_verify.go:29
type claims struct {
    UserID     string  // 用户ID
    UserType   int32   // 用户类型: 1-普通用户, 2-管理员
    PlatformID int32   // Chat系统中固定为0
    jwt.RegisteredClaims
}
3.1.3 缓存策略
// Redis缓存键: CHAT_UID_TOKEN_STATUS:{userID}
// 存储结构: Hash表
{
    "token0": 0,  // 0-正常状态Token,表示该Token有效且用户会话处于活跃状态。用户可以使用此Token进行所有授权操作。
    "token1": 1,  // 1-无效Token,表示该Token已被系统标记为不可用。通常由于安全原因或管理员操作导致
    "token2": 2,  // 2-被踢下线的Token,表示该Token对应的会话已被新登录实例强制终止
    "token3": 3,  // 3-过期Token,表示该Token已超过其有效期限。系统会自动清理过期Token。

}

3.2 OpenIM Admin Token - 管理员系统调用

3.2.1 类型和用途
  • 用途:Chat系统调用OpenIM Server管理接口的凭证
  • 生成者:OpenIM Server Auth RPC服务
  • 验证者:OpenIM Server Auth中间件
  • 适用场景:系统间管理调用、用户注册、Token生成
3.2.2 Token结构
// open-im-server/pkg/common/storage/controller/auth.go
// 使用tools/tokenverify包的Claims结构
type Claims struct {
    UserID     string  // 管理员用户ID(如: imAdmin)
    PlatformID int     // 管理员平台ID(AdminPlatformID)
    jwt.RegisteredClaims
}
3.2.3 缓存策略
// Redis缓存键: CHAT:IM_TOKEN:{adminUserID}
// 存储结构: String
// TTL: 4分钟 (可配置)

3.3 OpenIM User Token - 客户端IM连接

3.3.1 类型和用途
  • 用途:客户端WebSocket连接认证,用于IM消息收发
  • 生成者:OpenIM Server Auth RPC服务
  • 验证者:OpenIM Server WebSocket网关
  • 适用场景:客户端SDK连接、消息通信
3.3.2 Token结构
// 使用tools/tokenverify包的Claims结构
type Claims struct {
    UserID     string  // 用户ID
    PlatformID int     // 具体平台ID(iOS=1, Android=2, Web=5等)
    jwt.RegisteredClaims
}
3.3.3 缓存策略
// Redis缓存键: UID_PID_TOKEN_STATUS:{userID}:{platformName}
// 存储结构: Hash表
{
    "token1": 1,  // 1-正常状态
    "token2": 2,  // 2-被踢下线
}
// 示例: UID_PID_TOKEN_STATUS:2752796792:Android

3.4 三种Token详细对比

Token类型生成系统验证系统平台ID缓存键格式主要用途
Chat TokenChat AdminChat中间件0CHAT_UID_TOKEN_STATUS:{userID}Chat API认证
OpenIM Admin TokenOpenIM AuthOpenIM AuthAdminPlatformIDCHAT:IM_TOKEN:{adminUserID}系统管理调用
OpenIM User TokenOpenIM AuthOpenIM Auth具体平台IDUID_PID_TOKEN_STATUS:{userID}:{platform}WebSocket连接

3.5 Token使用场景矩阵

场景Chat TokenAdmin TokenUser Token备注
用户登录Chat系统用户身份认证
管理后台登录双Token机制
SDK连接WebSocketIM功能必需
Chat系统调用OpenIM系统间调用
用户注册流程完整注册需要
客户端消息收发IM核心功能
用户信息管理Chat API操作
强制下线操作管理员权限

4. 注册流程源码分析

4.1 第一阶段:Android客户端用户交互

4.1.1 用户界面 - RegisterActivity.java
// open-im-android-demo/Demo/app/src/main/java/io/openim/android/demo/ui/login/RegisterActivity.java:44
private void listener() {
    // 1. 监听文本变化,实时验证手机号格式
    view.edt1.addTextChangedListener(this);
    view.protocol.setOnCheckedChangeListener((buttonView, isChecked) -> submitEnabled());
    view.clear.setOnClickListener(v -> view.edt1.setText(""));
    
    // 2. 提交按钮点击事件 - 发送验证码
    view.submit.setOnClickListener(v -> {
        if (vm.isPhone.val()) {
            // 验证手机号格式
            if (!RegexValid.isValidPhoneNumber(vm.account.val())) {
                toast(getString(io.openim.android.ouicore.R.string.valid_phone_num));
                return;
            }
        }
        // 设置区号并发送验证码 (usedFor: 1-注册 2-重置密码)
        vm.areaCode.setValue("+" + view.countryCode.getSelectedCountryCode());
        vm.getVerificationCode(vm.isFindPassword ? 2 : 1);
    });
}
4.1.2 业务逻辑层 - LoginVM.java

发送验证码流程:

// open-im-android-demo/Demo/app/src/main/java/io/openim/android/demo/vm/LoginVM.java:119
public void getVerificationCode(int usedFor) {
    // 1. 构建验证码请求参数
    Parameter parameter = getParameter(null, usedFor);
    WaitDialog waitDialog = showWait();
    
    // 2. 调用Chat系统API发送验证码
    N.API(OpenIMService.class)
      .getVerificationCode(parameter.buildJsonBody())
      .map(OpenIMService.turn(Object.class))
      .compose(N.IOMain())
      .subscribe(new NetObserver<Object>(getContext()) {
          @Override
          public void onSuccess(Object o) {
              getIView().succ(o); // 通知UI发送成功
          }
          
          @Override
          protected void onFailure(Throwable e) {
              getIView().err(e.getMessage()); // 通知UI发送失败
          }
      });
}

用户注册流程:

// open-im-android-demo/Demo/app/src/main/java/io/openim/android/demo/vm/LoginVM.java:252
public void register() {
    String pwdValue = pwd.getValue();
    
    // 1. 客户端密码格式验证
    if (!RegexValid.isValidPassword(pwdValue)) {
        toast(BaseApp.inst().getString(R.string.password_valid_tips));
        return;
    }
    
    // 2. 构建注册请求参数
    Parameter parameter = new Parameter();
    parameter.add("verifyCode", verificationCode); // 验证码
    parameter.add("platform", Platform.ANDROID);   // 平台标识
    parameter.add("autoLogin", true);              // 自动登录标志
    
    // 3. 构建用户基本信息
    Map<String, String> user = new HashMap<>();
    user.put("password", md5(pwdValue));          // 密码MD5加密
    user.put("nickname", nickName.getValue());    // 昵称
    user.put("areaCode", areaCode.val());         // 区号
    
    if (isPhone.val()) {
        user.put("phoneNumber", account.getValue()); // 手机号
    } else {
        user.put("email", account.getValue());       // 邮箱
    }
    parameter.add("user", user);
    
    // 4. 调用Chat系统注册API
    WaitDialog waitDialog = showWait();
    N.API(OpenIMService.class)
      .register(parameter.buildJsonBody())
      .map(OpenIMService.turn(LoginCertificate.class))
      .compose(N.IOMain())
      .subscribe(new NetObserver<LoginCertificate>(context.get()) {
          @Override
          public void onSuccess(LoginCertificate o) {
              // 5. 注册成功,缓存登录凭证
              Log.d(TAG, "Register success, caching credentials");
              o.cache(getContext());
              getIView().jump(); // 跳转到主界面
          }
          
          @Override
          protected void onFailure(Throwable e) {
              getIView().toast(e.getMessage());
          }
      });
}
4.1.3 网络接口定义 - OpenIMService.java
// open-im-android-demo/Demo/app/src/main/java/io/openim/android/demo/repository/OpenIMService.java
public interface OpenIMService {
    // 用户注册接口
    @POST("account/register")
    Observable<ResponseBody> register(@Body RequestBody requestBody);
    
    // 发送验证码接口
    @POST("account/code/send")
    Observable<ResponseBody> getVerificationCode(@Body RequestBody requestBody);
    
    // 验证验证码接口
    @POST("account/code/verify")
    Observable<ResponseBody> checkVerificationCode(@Body RequestBody requestBody);
    
    // 通用响应转换器
    static <T> Function<ResponseBody, T> turn(Class<T> tClass) {
        return responseBody -> {
            String body = responseBody.string();
            Base<T> base = GsonHel.dataObject(body, tClass);
            if (base.errCode == 0)
                return null == base.data ? tClass.newInstance() : base.data;
            throw new RXRetrofitException(base.errCode, base.errDlt);
        };
    }
}

4.2 第二阶段:Chat系统处理

4.2.1 验证码生成与管理流程

HTTP入口 - 发送验证码接口

// chat/internal/api/chat/chat.go:45
func (o *Api) SendVerifyCode(c *gin.Context) {
    // 1. 解析HTTP请求参数
    req, err := a2r.ParseRequest[chatpb.SendVerifyCodeReq](c)
    if err != nil {
        apiresp.GinError(c, err)
        return
    }

    // 2. 获取客户端IP地址用于频率限制
    ip, err := o.GetClientIP(c)
    if err != nil {
        apiresp.GinError(c, err)
        return
    }
    req.Ip = ip

    // 3. 调用RPC服务发送验证码
    _, err = o.chatClient.SendVerifyCode(c, req)
    if err != nil {
        apiresp.GinError(c, err)
        return
    }

    apiresp.GinSuccess(c, nil)
}

RPC服务 - 验证码生成逻辑

// chat/internal/rpc/chat/login.go:89
func (o *chatSvr) SendVerifyCode(ctx context.Context, req *chat.SendVerifyCodeReq) (*chat.SendVerifyCodeResp, error) {
    // 1. IP频率限制检查
    if err := o.Admin.CheckSendCode(ctx, req.Ip, req.Account); err != nil {
        return nil, err
    }

    // 2. 根据使用场景验证账户状态
    switch req.UsedFor {
    case constant.VerificationCodeForRegister:
        // 注册场景:检查账户是否已存在
        _, err := o.Database.GetUserByAccount(ctx, req.Account)
        if err == nil {
            return nil, errs.ErrRegisteredAlready.WrapMsg("account already registered")
        }
        if !dbutil.IsDBNotFound(err) {
            return nil, err
        }
    case constant.VerificationCodeForResetPassword:
        // 重置密码场景:检查账户是否存在
        _, err := o.Database.GetUserByAccount(ctx, req.Account)
        if err != nil {
            if dbutil.IsDBNotFound(err) {
                return nil, errs.ErrUserNotFound.WrapMsg("account not found")
            }
            return nil, err
        }
    }

    // 3. 生成6位数字验证码
    code := o.genVerifyCode()
    
    // 4. 保存验证码到Redis(5分钟过期)
    if err := o.Database.SetVerifyCode(ctx, req.Account, code, req.UsedFor); err != nil {
        return nil, err
    }

    // 5. 发送验证码(短信/邮件)
    if err := o.sendVerifyCode(ctx, req.Account, code); err != nil {
        return nil, err
    }

    return &chat.SendVerifyCodeResp{}, nil
}

// 生成6位随机数字验证码
func (o *chatSvr) genVerifyCode() string {
    return fmt.Sprintf("%06d", rand.Intn(1000000))
}

验证码存储结构

// chat/pkg/common/db/database/chat.go:156
func (o *ChatDatabase) SetVerifyCode(ctx context.Context, account string, code string, usedFor int32) error {
    // 构建Redis Key:verify_code:{account}:{usedFor}
    key := o.verifyCodeKey(account, usedFor)
    
    // 存储验证码,5分钟过期
    return o.cache.SetEx(ctx, key, code, time.Minute*5)
}

func (o *ChatDatabase) verifyCodeKey(account string, usedFor int32) string {
    return fmt.Sprintf("verify_code:%s:%d", account, usedFor)
}

// 验证码验证
func (o *ChatDatabase) VerifyCode(ctx context.Context, account string, code string, usedFor int32) error {
    key := o.verifyCodeKey(account, usedFor)
    
    // 从Redis获取验证码
    storedCode, err := o.cache.Get(ctx, key)
    if err != nil {
        if errors.Is(err, redis.Nil) {
            return errs.ErrVerifyCodeExpired.WrapMsg("verify code expired or not exist")
        }
        return err
    }
    
    // 验证码比对
    if storedCode != code {
        return errs.ErrVerifyCodeWrong.WrapMsg("verify code wrong")
    }
    
    // 验证成功后删除验证码(一次性使用)
    _ = o.cache.Del(ctx, key)
    return nil
}

验证码发送服务

// chat/internal/rpc/chat/login.go:145
func (o *chatSvr) sendVerifyCode(ctx context.Context, account, code string) error {
    // 判断是手机号还是邮箱
    if strings.Contains(account, "@") {
        // 邮箱验证码发送
        return o.sendEmailCode(ctx, account, code)
    } else {
        // 短信验证码发送
        return o.sendSMSCode(ctx, account, code)
    }
}

func (o *chatSvr) sendSMSCode(ctx context.Context, phoneNumber, code string) error {
    // 调用第三方短信服务API
    // 这里可以集成阿里云、腾讯云等短信服务
    log.ZInfo(ctx, "Sending SMS code", "phone", phoneNumber, "code", code)
    
    // 实际项目中需要调用真实的短信API
    // return smsProvider.SendCode(phoneNumber, code)
    return nil
}

func (o *chatSvr) sendEmailCode(ctx context.Context, email, code string) error {
    // 调用邮件服务发送验证码
    log.ZInfo(ctx, "Sending email code", "email", email, "code", code)
    
    // 实际项目中需要调用真实的邮件API
    // return emailProvider.SendCode(email, code)
    return nil
}
4.2.2 用户注册完整流程

HTTP入口 - 用户注册接口

// chat/internal/api/chat/chat.go:91
func (o *Api) RegisterUser(c *gin.Context) {
    // 1. 解析HTTP请求参数
    req, err := a2r.ParseRequest[chatpb.RegisterUserReq](c)
    if err != nil {
        apiresp.GinError(c, err)
        return
    }

    // 2. 获取客户端IP地址用于安全检查
    ip, err := o.GetClientIP(c)
    if err != nil {
        apiresp.GinError(c, err)
        return
    }
    req.Ip = ip

    // 3. 获取OpenIM管理员Token,用于后续调用OpenIM Server
    // ImAdminTokenWithDefaultAdmin 是Chat系统调用OpenIM Server的关键认证机制
    // 
    // **Token获取流程:**
    // 1. 首先检查本地内存缓存(4分钟TTL)
    // 2. 如果缓存未命中,检查Redis缓存(CHAT:IM_TOKEN:{adminUserID})
    // 3. 如果Redis也未命中,调用OpenIM Server获取新Token
    // 4. 将新Token同时缓存到内存和Redis中
    //
    // **Token存储结构:**
    // - 内存缓存:map[userID]*authToken{token, timeout}
    // - Redis缓存:Key="CHAT:IM_TOKEN:{adminUserID}", Value=token, TTL=4分钟
    //
    // **安全验证:**
    // - 使用系统配置的Secret验证身份
    // - 验证adminUserID是否在IMAdminUserID列表中
    // - 确认管理员用户在OpenIM Server中存在
    //
    // **Token特性:**
    // - JWT格式,包含UserID、PlatformID(AdminPlatformID)、过期时间
    // - 管理员Token不受多端登录策略限制
    // - 跳过Redis状态检查,提升性能
    imToken, err := o.imApiCaller.ImAdminTokenWithDefaultAdmin(c)
    if err != nil {
        apiresp.GinError(c, err)
        return
    }
    apiCtx := mctx.WithApiToken(c, imToken)
    rpcCtx := o.WithAdminUser(c)

    // 4. 检查用户是否已经在Chat系统中注册
    checkResp, err := o.chatClient.CheckUserExist(rpcCtx, &chatpb.CheckUserExistReq{User: req.User})
    if err != nil {
        log.ZDebug(rpcCtx, "CheckUserExist error", errs.Unwrap(err))
        apiresp.GinError(c, err)
        return
    }

    // 5. 处理数据一致性问题
    if checkResp.IsRegistered {
        // 检查用户在OpenIM Server中是否存在
        isUserNotExist, err := o.imApiCaller.AccountCheckSingle(apiCtx, checkResp.Userid)
        if err != nil {
            apiresp.GinError(c, err)
            return
        }
        // 如果用户在Chat存在但OpenIM不存在,删除Chat中的记录
        if isUserNotExist {
            _, err := o.chatClient.DelUserAccount(rpcCtx, &chatpb.DelUserAccountReq{UserIDs: []string{checkResp.Userid}})
            log.ZDebug(c, "Deleted inconsistent user data", checkResp.Userid)
            if err != nil {
                apiresp.GinError(c, err)
                return
            }
        }
    }

    // 6. 在Chat系统中注册用户
    respRegisterUser, err := o.chatClient.RegisterUser(c, req)
    if err != nil {
        apiresp.GinError(c, err)
        return
    }

    // 7. 在OpenIM Server中注册用户
    // 
    // **为什么需要在OpenIM Server中注册?**
    // Chat系统和OpenIM Server是两个独立的系统:
    // - Chat系统:负责用户账户管理、认证、业务逻辑
    // - OpenIM Server:负责实时消息通信、WebSocket连接、消息路由
    // 
    // **OpenIM Server注册的作用:**
    // 1. 创建用户在IM系统中的身份标识
    // 2. 建立用户与WebSocket网关的连接能力
    // 3. 初始化用户的消息接收和发送权限
    // 4. 为后续的好友关系、群组关系建立基础
    //
    // **注册数据结构:**
    // - UserID: 与Chat系统保持一致的用户唯一标识
    // - Nickname: 用户昵称,用于消息显示
    // - FaceURL: 用户头像,用于界面展示
    // - CreateTime: 创建时间戳
    // - AppMangerLevel: 应用管理级别(默认为普通用户)
    // - GlobalRecvMsgOpt: 全局消息接收选项
    //
    // **权限验证:**
    // - 只有管理员才能调用OpenIM Server的用户注册接口
    // - 使用ImAdminToken进行身份验证
    // - 检查用户ID格式(不能包含':'字符)
    // - 验证用户是否已在OpenIM Server中存在
    userInfo := &sdkws.UserInfo{
        UserID:     respRegisterUser.UserID,
        Nickname:   req.User.Nickname,
        FaceURL:    req.User.FaceURL,
        CreateTime: time.Now().UnixMilli(),
    }
    err = o.imApiCaller.RegisterUser(apiCtx, []*sdkws.UserInfo{userInfo})
    if err != nil {
        apiresp.GinError(c, err)
        return
    }

    // 8. 建立默认社交关系
    if resp, err := o.adminClient.FindDefaultFriend(rpcCtx, &admin.FindDefaultFriendReq{}); err == nil {
        _ = o.imApiCaller.ImportFriend(apiCtx, respRegisterUser.UserID, resp.UserIDs)
    }
    if resp, err := o.adminClient.FindDefaultGroup(rpcCtx, &admin.FindDefaultGroupReq{}); err == nil {
        _ = o.imApiCaller.InviteToGroup(apiCtx, respRegisterUser.UserID, resp.GroupIDs)
    }

    // 9. 生成用户ImToken(如果启用自动登录)
    var resp apistruct.UserRegisterResp
    if req.AutoLogin {
        resp.ImToken, err = o.imApiCaller.GetUserToken(apiCtx, respRegisterUser.UserID, req.Platform)
        if err != nil {
            apiresp.GinError(c, err)
            return
        }
    }
    resp.ChatToken = respRegisterUser.ChatToken
    resp.UserID = respRegisterUser.UserID
    apiresp.GinSuccess(c, &resp)
}

ImAdminTokenWithDefaultAdmin获取完整流程

// chat/pkg/common/imapi/caller.go:75
func (c *Caller) ImAdminTokenWithDefaultAdmin(ctx context.Context) (string, error) {
    return c.GetAdminTokenCache(ctx, c.defaultIMUserID)
}

func (c *Caller) GetAdminTokenCache(ctx context.Context, userID string) (string, error) {
    // 1. 读锁检查内存缓存
    c.lock.RLock()
    t, ok := c.tokenCache[userID]
    c.lock.RUnlock()
    
    // 2. 内存缓存命中且未过期
    if ok && !t.timeout.Before(time.Now()) {
        return t.token, nil
    }
    
    // 3. 写锁更新缓存
    c.lock.Lock()
    defer c.lock.Unlock()
    
    // 4. 双重检查避免并发问题
    t, ok = c.tokenCache[userID]
    if ok && !t.timeout.Before(time.Now()) {
        return t.token, nil
    }
    
    // 5. 从Redis获取Token
    token, err := c.tokenDB.GetIMToken(ctx, userID)
    if err != nil && !errors.Is(err, redis.Nil) {
        return "", err
    }
    
    if errors.Is(err, redis.Nil) {
        // 6. Redis缓存未命中,调用OpenIM Server获取新Token
        token, err = c.GetAdminTokenServer(ctx, userID)
        if err != nil {
            return "", err
        }
    }
    
    // 7. 更新内存缓存(4分钟TTL)
    t = &authToken{token: token, timeout: time.Now().Add(time.Minute * 4)}
    c.tokenCache[userID] = t
    
    return t.token, nil
}

func (c *Caller) GetAdminTokenServer(ctx context.Context, userID string) (string, error) {
    // 8. 调用OpenIM Server Auth服务获取管理员Token
    resp, err := getAdminToken.Call(ctx, c.imApi, &auth.GetAdminTokenReq{
        Secret: c.imSecret,  // 系统密钥验证
        UserID: userID,      // 管理员用户ID
    })
    if err != nil {
        return "", err
    }
    
    // 9. 将Token缓存到Redis
    err = c.tokenDB.SetIMToken(ctx, userID, resp.Token)
    if err != nil {
        log.ZWarn(ctx, "set im admin token to redis failed", err, "userID", userID)
    }
    
    return resp.Token, nil
}

用户注册RPC服务主流程

// chat/internal/rpc/chat/login.go:258
func (o *chatSvr) RegisterUser(ctx context.Context, req *chat.RegisterUserReq) (*chat.RegisterUserResp, error) {
    // 1. 权限检查和上下文设置
    isAdmin, err := o.Admin.CheckNilOrAdmin(ctx)
    ctx = o.WithAdminUser(ctx)
    if err != nil {
        return nil, err
    }
    
    // 2. 验证注册信息(手机号/邮箱/账户名格式检查)
    if err = o.checkRegisterInfo(ctx, req.User, isAdmin); err != nil {
        return nil, err
    }
    
    var usedInvitationCode bool
    if !isAdmin {
        // 3. 非管理员用户的注册权限检查
        if !o.AllowRegister {
            return nil, errs.ErrNoPermission.WrapMsg("register user is disabled")
        }
        if req.User.UserID != "" {
            return nil, errs.ErrNoPermission.WrapMsg("only admin can set user id")
        }
        
        // 4. IP频率限制检查
        if err := o.Admin.CheckRegister(ctx, req.Ip); err != nil {
            return nil, err
        }
        
        // 5. 邀请码检查(如果系统配置需要)
        conf, err := o.Admin.GetConfig(ctx)
        if err != nil {
            return nil, err
        }
        if val := conf[constant.NeedInvitationCodeRegisterConfigKey]; datautil.Contain(strings.ToLower(val), "1", "true", "yes") {
            usedInvitationCode = true
            if req.InvitationCode == "" {
                return nil, errs.ErrArgs.WrapMsg("invitation code is empty")
            }
            if err := o.Admin.CheckInvitationCode(ctx, req.InvitationCode); err != nil {
                return nil, err
            }
        }
        
        // 6. 验证码验证
        if req.User.Email == "" {
            // 手机号注册验证码检查
            if _, err := o.verifyCode(ctx, o.verifyCodeJoin(req.User.AreaCode, req.User.PhoneNumber), req.VerifyCode, phone); err != nil {
                return nil, err
            }
        } else {
            // 邮箱注册验证码检查
            if _, err := o.verifyCode(ctx, req.User.Email, req.VerifyCode, mail); err != nil {
                return nil, err
            }
        }
    }
    
    // 7. 生成用户ID(如果没有指定)
    if req.User.UserID == "" {
        for i := 0; i < 20; i++ {
            userID := o.genUserID() // 生成10位数字ID
            _, err := o.Database.GetUser(ctx, userID)
            if err == nil {
                continue // ID已存在,重新生成
            } else if dbutil.IsDBNotFound(err) {
                req.User.UserID = userID
                break
            } else {
                return nil, err
            }
        }
        if req.User.UserID == "" {
            return nil, errs.ErrInternalServer.WrapMsg("gen user id failed")
        }
    }
    
    // 8. 构建凭证信息
    var (
        credentials  []*chatdb.Credential
        registerType int32
    )

    if req.User.PhoneNumber != "" {
        registerType = constant.PhoneRegister
        credentials = append(credentials, &chatdb.Credential{
            UserID:      req.User.UserID,
            Account:     BuildCredentialPhone(req.User.AreaCode, req.User.PhoneNumber),
            Type:        constant.CredentialPhone,
            AllowChange: true,
        })
    }

    if req.User.Account != "" {
        credentials = append(credentials, &chatdb.Credential{
            UserID:      req.User.UserID,
            Account:     req.User.Account,
            Type:        constant.CredentialAccount,
            AllowChange: true,
        })
        registerType = constant.AccountRegister
    }

    if req.User.Email != "" {
        registerType = constant.EmailRegister
        credentials = append(credentials, &chatdb.Credential{
            UserID:      req.User.UserID,
            Account:     req.User.Email,
            Type:        constant.CredentialEmail,
            AllowChange: true,
        })
    }
    
    // 9. 构建数据库记录对象
    now := time.Now()
    register := &chatdb.Register{
        UserID:      req.User.UserID,
        DeviceID:    req.DeviceID,
        IP:          req.Ip,
        Platform:    constantpb.PlatformID2Name[int(req.Platform)],
        AccountType: "",
        Mode:        constant.UserMode,
        CreateTime:  now,
    }
    
    account := &chatdb.Account{
        UserID:         req.User.UserID,
        Password:       req.User.Password,
        OperatorUserID: mcontext.GetOpUserID(ctx),
        ChangeTime:     now,
        CreateTime:     now,
    }

    attribute := &chatdb.Attribute{
        UserID:         req.User.UserID,
        Account:        req.User.Account,
        PhoneNumber:    req.User.PhoneNumber,
        AreaCode:       req.User.AreaCode,
        Email:          req.User.Email,
        Nickname:       req.User.Nickname,
        FaceURL:        req.User.FaceURL,
        Gender:         req.User.Gender,
        BirthTime:      time.UnixMilli(req.User.Birth),
        ChangeTime:     now,
        CreateTime:     now,
        AllowVibration: constant.DefaultAllowVibration,
        AllowBeep:      constant.DefaultAllowBeep,
        AllowAddFriend: constant.DefaultAllowAddFriend,
        RegisterType:   registerType,
    }
    
    // 10. 事务性保存到数据库
    if err := o.Database.RegisterUser(ctx, register, account, attribute, credentials); err != nil {
        return nil, err
    }
    
    // 11. 处理邀请码使用记录
    if usedInvitationCode {
        if err := o.Admin.UseInvitationCode(ctx, req.User.UserID, req.InvitationCode); err != nil {
            log.ZError(ctx, "UseInvitationCode", err, "userID", req.User.UserID, "invitationCode", req.InvitationCode)
        }
    }
    
    // 12. 生成ChatToken(如果启用自动登录)
    var resp chat.RegisterUserResp
    if req.AutoLogin {
        chatToken, err := o.Admin.CreateToken(ctx, req.User.UserID, constant.NormalUser)
        if err == nil {
            resp.ChatToken = chatToken.Token
        } else {
            log.ZError(ctx, "Admin CreateToken Failed", err, "userID", req.User.UserID, "platform", req.Platform)
        }
    }
    
    resp.UserID = req.User.UserID
    return &resp, nil
}

数据库事务操作 - database/chat.go

// chat/pkg/common/db/database/chat.go:214
func (o *ChatDatabase) RegisterUser(ctx context.Context, register *chatdb.Register, account *chatdb.Account, attribute *chatdb.Attribute, credentials []*chatdb.Credential) error {
    // 使用MongoDB事务确保数据一致性
    return o.tx.Transaction(ctx, func(ctx context.Context) error {
        // 1. 创建注册记录
        if err := o.register.Create(ctx, register); err != nil {
            return err
        }
        
        // 2. 创建用户账户(密码)
        if err := o.account.Create(ctx, account); err != nil {
            return err
        }
        
        // 3. 创建用户属性
        if err := o.attribute.Create(ctx, attribute); err != nil {
            return err
        }
        
        // 4. 创建登录凭证
        if err := o.credential.Create(ctx, credentials...); err != nil {
            return err
        }
        
        return nil
    })
}

4.3 第三阶段:OpenIM Server集成

4.3.1 Chat系统调用OpenIM - caller.go
// chat/pkg/common/imapi/caller.go:175
func (c *Caller) RegisterUser(ctx context.Context, users []*sdkws.UserInfo) error {
    // 调用OpenIM Server用户注册接口
    _, err := registerUser.Call(ctx, c.imApi, &user.UserRegisterReq{
        Users: users,
    })
    return err
}

获取UserToken

// chat/pkg/common/imapi/caller.go:126
func (c *Caller) GetUserToken(ctx context.Context, userID string, platformID int32) (string, error) {
    // 为用户生成IM Token用于WebSocket连接
    resp, err := getuserToken.Call(ctx, c.imApi, &auth.GetUserTokenReq{
        PlatformID: platformID,
        UserID:     userID,
    })
    if err != nil {
        return "", err
    }
    return resp.Token, nil
}

获取AdminToken

// open-im-server/internal/rpc/auth/auth.go:142
func (s *authServer) GetAdminToken(ctx context.Context, req *pbauth.GetAdminTokenReq) (*pbauth.GetAdminTokenResp, error) {
    // 1. 验证系统密钥
    if req.Secret != s.config.Share.Secret {
        return nil, errs.ErrNoPermission.WrapMsg("secret invalid")
    }
    
    // 2. 验证用户ID是否在管理员列表中
    if !datautil.Contain(req.UserID, s.config.Share.IMAdminUserID...) {
        return nil, errs.ErrArgs.WrapMsg("userID is error")
    }
    
    // 3. 验证管理员用户是否存在
    if err := s.userClient.CheckUser(ctx, []string{req.UserID}); err != nil {
        return nil, err
    }
    
    // 4. 生成JWT Token
    token, err := s.authDatabase.CreateToken(ctx, req.UserID, int(constant.AdminPlatformID))
    if err != nil {
        return nil, err
    }
    
    return &pbauth.GetAdminTokenResp{
        Token: token,
        ExpireTimeSeconds: s.config.RpcConfig.TokenPolicy.Expire * 24 * 60 * 60,
    }, nil
}
4.3.2 OpenIM Server用户注册 - user.go
// open-im-server/internal/rpc/user/user.go:453
func (s *userServer) UserRegister(ctx context.Context, req *pbuser.UserRegisterReq) (resp *pbuser.UserRegisterResp, err error) {
    resp = &pbuser.UserRegisterResp{}
    
    // 1. 参数验证
    if len(req.Users) == 0 {
        return nil, errs.ErrArgs.WrapMsg("users is empty")
    }

    // 2. 管理员权限检查
    if err = authverify.CheckAdmin(ctx, s.config.Share.IMAdminUserID); err != nil {
        return nil, err
    }

    // 3. 检查用户ID重复
    if datautil.DuplicateAny(req.Users, func(e *sdkws.UserInfo) string { return e.UserID }) {
        return nil, errs.ErrArgs.WrapMsg("userID repeated")
    }
    
    // 4. 用户ID格式验证
    userIDs := make([]string, 0)
    for _, user := range req.Users {
        if user.UserID == "" {
            return nil, errs.ErrArgs.WrapMsg("userID is empty")
        }
        if strings.Contains(user.UserID, ":") {
            return nil, errs.ErrArgs.WrapMsg("userID contains ':' is invalid userID")
        }
        userIDs = append(userIDs, user.UserID)
    }
    
    // 5. 检查用户是否已存在
    exist, err := s.db.IsExist(ctx, userIDs)
    if err != nil {
        return nil, err
    }
    if exist {
        return nil, servererrs.ErrRegisteredAlready.WrapMsg("userID registered already")
    }
    
    // 6. 注册前webhook
    if err := s.webhookBeforeUserRegister(ctx, &s.config.WebhooksConfig.BeforeUserRegister, req); err != nil {
        return nil, err
    }
    
    // 7. 构建用户数据
    now := time.Now()
    users := make([]*tablerelation.User, 0, len(req.Users))
    for _, user := range req.Users {
        users = append(users, &tablerelation.User{
            UserID:           user.UserID,
            Nickname:         user.Nickname,
            FaceURL:          user.FaceURL,
            Ex:               user.Ex,
            CreateTime:       now,
            AppMangerLevel:   user.AppMangerLevel,
            GlobalRecvMsgOpt: user.GlobalRecvMsgOpt,
        })
    }
    
    // 8. 保存到OpenIM数据库
    if err := s.db.Create(ctx, users); err != nil {
        return nil, err
    }
    
    // 9. 更新监控指标
    prommetrics.UserRegisterCounter.Add(float64(len(users)))
    
    // 10. 注册后webhook
    s.webhookAfterUserRegister(ctx, &s.config.WebhooksConfig.AfterUserRegister, req)
    
    return resp, nil
}

4.4 注册的系统交互时序图

以下时序图展示了OpenIM设备注册的完整流程,包括验证码发送和用户注册两个主要阶段:

sequenceDiagram
    participant Client as Android Demo<br/>RegisterActivity
    participant SDK as open-im-sdk-android<br/>LoginVM
    participant ChatAPI as Chat System<br/>HTTP API
    participant ChatRPC as Chat System<br/>RPC Service
    participant Redis as Redis Cache<br/>验证码/Token存储
    participant ChatDB as Chat Database<br/>MongoDB
    participant OpenIMAPI as OpenIM Server<br/>RPC API
    participant OpenIMDB as OpenIM Database<br/>MongoDB

    Note over Client,OpenIMDB: === 阶段1: 验证码发送流程 ===
    
    Client->>SDK: 1.1 用户点击发送验证码<br/>vm.getVerificationCode(1)
    SDK->>ChatAPI: 1.2 POST /account/code/send<br/>{"account":"手机号","usedFor":1}
    ChatAPI->>ChatRPC: 1.3 SendVerifyCode RPC调用<br/>检查IP频率限制
    ChatRPC->>ChatDB: 1.4 验证账户状态<br/>检查是否已注册
    ChatDB-->>ChatRPC: 1.5 返回查询结果
    ChatRPC->>ChatRPC: 1.6 生成6位验证码<br/>fmt.Sprintf("%06d", rand.Intn(1000000))
    ChatRPC->>Redis: 1.7 存储验证码<br/>Key: verify_code:{account}:1<br/>TTL: 5分钟
    Redis-->>ChatRPC: 1.8 存储成功
    ChatRPC->>ChatRPC: 1.9 发送短信/邮件<br/>调用第三方服务
    ChatRPC-->>ChatAPI: 1.10 发送成功响应
    ChatAPI-->>SDK: 1.11 HTTP 200 OK
    SDK-->>Client: 1.12 UI显示发送成功<br/>启动倒计时

    Note over Client,OpenIMDB: === 阶段2: 用户注册流程 ===

    Client->>SDK: 2.1 用户输入信息并注册<br/>vm.register()
    SDK->>SDK: 2.2 客户端验证<br/>密码格式、昵称等
    SDK->>ChatAPI: 2.3 POST /account/register<br/>{"user":{...},"verifyCode":"123456","platform":2}
    ChatAPI->>ChatAPI: 2.4 获取管理员Token<br/>ImAdminTokenWithDefaultAdmin()
    ChatAPI->>OpenIMAPI: 2.5 获取Admin Token<br/>GetAdminToken(secret, adminUserID)
    OpenIMAPI->>OpenIMAPI: 2.6 验证密钥和管理员身份
    OpenIMAPI->>OpenIMDB: 2.7 检查管理员用户存在性
    OpenIMDB-->>OpenIMAPI: 2.8 用户存在确认
    OpenIMAPI->>Redis: 2.9 缓存Admin Token<br/>Key: CHAT:IM_TOKEN:{adminUserID}<br/>TTL: 4分钟
    Redis-->>OpenIMAPI: 2.10 缓存成功
    OpenIMAPI-->>ChatAPI: 2.11 返回Admin Token<br/>JWT格式
    ChatAPI->>ChatRPC: 2.12 RegisterUser RPC调用<br/>携带所有注册信息
    ChatRPC->>Redis: 2.13 验证验证码<br/>Key: verify_code:{account}:1
    Redis-->>ChatRPC: 2.14 返回存储的验证码
    ChatRPC->>ChatRPC: 2.15 验证码比对<br/>验证成功后删除
    ChatRPC->>Redis: 2.16 删除已使用的验证码<br/>确保一次性使用
    ChatRPC->>ChatRPC: 2.17 生成用户ID<br/>10位数字ID
    ChatRPC->>ChatDB: 2.18 事务性保存用户数据<br/>Register+Account+Attribute+Credential
    
    Note over ChatDB: MongoDB事务操作<br/>register: 注册记录<br/>account: 密码信息<br/>attribute: 用户属性<br/>credential: 登录凭证
    
    ChatDB-->>ChatRPC: 2.19 保存成功确认
    ChatRPC->>ChatRPC: 2.20 生成Chat Token<br/>JWT: {userID, userType:1, platformID:0}
    ChatRPC->>Redis: 2.21 缓存Chat Token状态<br/>Key: CHAT_UID_TOKEN_STATUS:{userID}<br/>Value: {token: 1}
    Redis-->>ChatRPC: 2.22 缓存成功
    ChatRPC-->>ChatAPI: 2.23 返回注册结果<br/>{userID, chatToken}
    ChatAPI->>OpenIMAPI: 2.24 在OpenIM Server注册用户<br/>RegisterUser([UserInfo])
    OpenIMAPI->>OpenIMAPI: 2.25 管理员权限检查<br/>验证Admin Token
    OpenIMAPI->>OpenIMAPI: 2.26 用户数据验证<br/>UserID格式、重复检查
    OpenIMAPI->>OpenIMDB: 2.27 检查用户是否已存在
    OpenIMDB-->>OpenIMAPI: 2.28 用户不存在确认
    OpenIMAPI->>OpenIMDB: 2.29 保存IM用户信息<br/>User{UserID, Nickname, FaceURL...}
    OpenIMDB-->>OpenIMAPI: 2.30 保存成功
    OpenIMAPI-->>ChatAPI: 2.31 IM用户注册成功
    ChatAPI->>OpenIMAPI: 2.32 获取用户IM Token<br/>GetUserToken(userID, platformID)
    OpenIMAPI->>OpenIMAPI: 2.33 验证请求权限<br/>只有管理员可调用
    OpenIMAPI->>OpenIMAPI: 2.34 生成用户IM Token<br/>JWT: {userID, platformID:2}
    OpenIMAPI->>Redis: 2.35 缓存IM Token状态<br/>Key: UID_PID_TOKEN_STATUS:{userID}:Android<br/>Value: {token: 1}
    Redis-->>OpenIMAPI: 2.36 缓存成功
    OpenIMAPI-->>ChatAPI: 2.37 返回IM Token
    ChatAPI-->>SDK: 2.38 返回完整注册结果<br/>{userID, chatToken, imToken}
    SDK->>SDK: 2.39 缓存登录凭证<br/>LoginCertificate.cache()
    SDK-->>Client: 2.40 注册成功,跳转主界面

    Note over Client,OpenIMDB: === Token缓存结构说明 ===
    Note over Redis: Chat Token: CHAT_UID_TOKEN_STATUS:{userID}<br/>Admin Token: CHAT:IM_TOKEN:{adminUserID}<br/>IM Token: UID_PID_TOKEN_STATUS:{userID}:{platform}<br/>验证码: verify_code:{account}:{usedFor}

5. 登录流程源码分析

5.1 第一阶段:Android客户端用户交互

5.1.1 用户界面 - LoginActivity.java
// open-im-android-demo2/Demo/app/src/main/java/io/openim/android/demo/ui/login/LoginActivity.java:177-253
view.submit.setOnClickListener(v -> {
    vm.areaCode.setValue("+"+view.loginContent.countryCode.getSelectedCountryCode());
    waitDialog.show();
    vm.login(isVCLogin ? vm.pwd.getValue() : null, 3);
});

关键点分析:

  • 设置区号:从国家代码选择器获取区号
  • 显示等待对话框:提供用户反馈
  • 调用ViewModel的login方法:传入验证码和用途标识(3表示登录)
5.1.2 登录业务逻辑 - LoginVM.java

LoginViewModel负责处理登录的核心业务逻辑:

// open-im-android-demo2/Demo/app/src/main/java/io/openim/android/demo/vm/LoginVM.java:51-85
public void login(String verificationCode, int usedFor) {
    // 1. 保存登录类型到本地缓存
    SharedPreferencesUtil.get(BaseApp.inst())
        .setCache(Constants.K_LOGIN_TYPE, isPhone.val() ? 0 : 1);
    
    // 2. 构建登录参数
    Parameter parameter = getParameter(verificationCode, usedFor);
    
    // 3. 调用Chat系统登录API
    N.API(OpenIMService.class)
        .login(parameter.buildJsonBody())
        .compose(N.IOMain())
        .map(OpenIMService.turn(LoginCertificate.class))
        .subscribe(new NetObserver<LoginCertificate>(getContext()) {
            @Override
            public void onSuccess(LoginCertificate loginCertificate) {
                // 4. 获取到ChatToken和ImToken后,连接OpenIM SDK
                try {
                    OpenIMClient.getInstance().login(new OnBase<String>() {
                        @Override
                        public void onError(int code, String error) {
                            getIView().err(error);
                        }
                        
                        @Override
                        public void onSuccess(String data) {
                            // 5. SDK连接成功,缓存登录信息
                            loginCertificate.cache(getContext());
                            BaseApp.inst().loginCertificate = loginCertificate;
                            getIView().jump();
                        }
                    }, loginCertificate.userID, loginCertificate.imToken);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            
            @Override
            protected void onFailure(Throwable e) {
                getIView().err(e.getMessage());
            }
        });
}

参数构建详解:

// open-im-android-demo2/Demo/app/src/main/java/io/openim/android/demo/vm/LoginVM.java:94-110
@NonNull
private Parameter getParameter(String verificationCode, int usedFor) {
    Parameter parameter = new Parameter().add("password",
            TextUtils.isEmpty(verificationCode) ? md5(pwd.val()) : null)
        .add("platform", 2).add("usedFor", usedFor)
        .add("operationID", System.currentTimeMillis() + "")
        .add("verifyCode", verificationCode);
    if (isPhone.val()) {
        parameter.add("phoneNumber", account.getValue());
        parameter.add("areaCode", areaCode.val());
    } else
        parameter.add("email", account.getValue());
    return parameter;
}

关键参数说明:

  • platform: 平台ID,Android固定为2
  • usedFor: 用途标识,登录为3
  • password: 密码MD5哈希(验证码登录时为null)
  • verifyCode: 验证码(密码登录时为null)
  • phoneNumber/email: 登录账号
  • operationID: 操作ID,用于链路追踪
5.1.3 API接口定义 - OpenIMService.java
// open-im-android-demo2/Demo/app/src/main/java/io/openim/android/demo/repository/OpenIMService.java:18-19
@POST("account/login")
Observable<ResponseBody> login(@Body RequestBody requestBody);

使用Retrofit定义RESTful API接口,支持RxJava响应式编程。

5.2 第二阶段:Chat API网关处理

5.2.1 HTTP请求路由 - chat.go

Chat API网关接收到登录请求后进行处理:

// chat/internal/api/chat/chat.go:197-247
func (o *Api) Login(c *gin.Context) {
    // 1. 解析登录请求参数(账号/手机/邮箱 + 密码/验证码)
    req, err := a2r.ParseRequest[chatpb.LoginReq](c)
    if err != nil {
        apiresp.GinError(c, err)
        return
    }

    // 2. 获取客户端IP地址,用于安全检查和登录记录
    ip, err := o.GetClientIP(c)
    if err != nil {
        apiresp.GinError(c, err)
        return
    }
    req.Ip = ip

    // 3. 调用Chat RPC服务进行用户认证,获取ChatToken
    resp, err := o.chatClient.Login(c, req)
    if err != nil {
        apiresp.GinError(c, err)
        return
    }

    // 4. 获取OpenIM管理员Token,用于调用OpenIM Server的管理接口
    adminToken, err := o.imApiCaller.ImAdminTokenWithDefaultAdmin(c)
    if err != nil {
        apiresp.GinError(c, err)
        return
    }
    apiCtx := mctx.WithApiToken(c, adminToken)

    // 5. 使用管理员Token调用OpenIM Server,为用户生成ImToken
    imToken, err := o.imApiCaller.GetUserToken(apiCtx, resp.UserID, req.Platform)
    if err != nil {
        apiresp.GinError(c, err)
        return
    }

    // 6. 返回登录结果,包含两个Token
    apiresp.GinSuccess(c, &apistruct.LoginResp{
        ImToken:   imToken,     // OpenIM Server WebSocket连接Token
        UserID:    resp.UserID,
        ChatToken: resp.ChatToken, // Chat系统API认证Token
    })
}

双Token机制详解:

  • ChatToken: 用于Chat系统REST API认证,访问用户管理、配置等功能
  • ImToken: 用于OpenIM Server WebSocket连接认证,进行实时消息通信

5.3 第三阶段:Chat RPC服务认证

5.3.1 用户认证核心逻辑 - login.go

Chat RPC服务负责核心的用户认证逻辑,这是登录流程的关键环节:

// chat/internal/rpc/chat/login.go:434-511
func (o *chatSvr) Login(ctx context.Context, req *chat.LoginReq) (*chat.LoginResp, error) {
    resp := &chat.LoginResp{}
    
    // 1. 参数验证:密码和验证码至少要有一个
    if req.Password == "" && req.VerifyCode == "" {
        return nil, errs.ErrArgs.WrapMsg("password or code must be set")
    }
    
    var (
        err        error
        credential *chatdb.Credential  // 用户凭证信息
        acc        string              // 登录账号
    )

    // 2. 根据不同的登录方式构建账号字符串
    switch {
    case req.Account != "":
        // 用户名登录
        acc = req.Account
    case req.PhoneNumber != "":
        // 手机号登录
        if req.AreaCode == "" {
            return nil, errs.ErrArgs.WrapMsg("area code must")
        }
        if !strings.HasPrefix(req.AreaCode, "+") {
            req.AreaCode = "+" + req.AreaCode
        }
        if _, err := strconv.ParseUint(req.AreaCode[1:], 10, 64); err != nil {
            return nil, errs.ErrArgs.WrapMsg("area code must be number")
        }
        // 构建手机号凭证格式:+86 13800000000
        acc = BuildCredentialPhone(req.AreaCode, req.PhoneNumber)
    case req.Email != "":
        // 邮箱登录
        acc = req.Email
    default:
        return nil, errs.ErrArgs.WrapMsg("account or phone number or email must be set")
    }
    
    // 3. 根据账号查询用户凭证信息
    credential, err = o.Database.TakeCredentialByAccount(ctx, acc)
    if err != nil {
        if dbutil.IsDBNotFound(err) {
            return nil, eerrs.ErrAccountNotFound.WrapMsg("user unregistered")
        }
        return nil, err
    }
    
    // 4. IP和用户登录频率检查
    if err := o.Admin.CheckLogin(ctx, credential.UserID, req.Ip); err != nil {
        return nil, err
    }
    
    var verifyCodeID *string
    
    // 5. 根据登录方式进行认证
    if req.Password == "" {
        // 验证码登录方式
        var id string
        
        if req.Email == "" {
            // 手机验证码登录
            account := o.verifyCodeJoin(req.AreaCode, req.PhoneNumber)
            id, err = o.verifyCode(ctx, account, req.VerifyCode, phone)
            if err != nil {
                return nil, err
            }
        } else {
            // 邮箱验证码登录
            account := req.Email
            id, err = o.verifyCode(ctx, account, req.VerifyCode, mail)
            if err != nil {
                return nil, err
            }
        }
        
        if id != "" {
            verifyCodeID = &id
        }
    } else {
        // 密码登录方式
        account, err := o.Database.TakeAccount(ctx, credential.UserID)
        if err != nil {
            return nil, err
        }
        
        // 密码比对(MD5哈希值比较)
        if account.Password != req.Password {
            return nil, eerrs.ErrPassword.Wrap()
        }
    }
    
    // 6. 生成ChatToken
    chatToken, err := o.Admin.CreateToken(ctx, credential.UserID, constant.NormalUser)
    if err != nil {
        return nil, err
    }
    
    // 7. 记录登录日志
    record := &chatdb.UserLoginRecord{
        UserID:    credential.UserID,
        LoginTime: time.Now(),
        IP:        req.Ip,
        DeviceID:  req.DeviceID,
        Platform:  constantpb.PlatformIDToName(int(req.Platform)),
    }
    if err := o.Database.LoginRecord(ctx, record, verifyCodeID); err != nil {
        return nil, err
    }
    
    // 8. 清理已使用的验证码
    if verifyCodeID != nil {
        if err := o.Database.DelVerifyCode(ctx, *verifyCodeID); err != nil {
            return nil, err
        }
    }
    
    // 9. 返回登录成功响应
    resp.UserID = credential.UserID
    resp.ChatToken = chatToken.Token
    return resp, nil
}
5.3.2 关键认证机制详解

账号凭证构建策略:

  • 用户名登录: 直接使用账号字符串
  • 手机号登录: 格式化为+{区号} {手机号}(如:+86 13800000000)
  • 邮箱登录: 直接使用邮箱地址

双重认证支持:

  • 密码认证: MD5哈希值比对,适用于常规登录
  • 验证码认证: 动态验证码验证,适用于快捷登录或找回密码

安全防护机制:

  • IP频率限制: 防止暴力破解攻击
  • 验证码一次性使用: 防止重放攻击
  • 登录日志记录: 支持安全审计和异常检测

5.4 第四阶段:OpenIM Admin Token生成

5.4.1 管理员Token请求 - auth.go
// open-im-server/internal/rpc/auth/auth.go:200-236
func (s *authServer) GetAdminToken(ctx context.Context, req *pbauth.GetAdminTokenReq) (*pbauth.GetAdminTokenResp, error) {
    resp := pbauth.GetAdminTokenResp{}

    // 验证系统密钥
    if req.Secret != s.config.Share.Secret {
        return nil, errs.ErrNoPermission.WrapMsg("secret invalid")
    }

    // 验证用户ID是否在管理员列表中
    if !datautil.Contain(req.UserID, s.config.Share.IMAdminUserID...) {
        return nil, errs.ErrArgs.WrapMsg("userID is error.", "userID", req.UserID, "adminUserID", s.config.Share.IMAdminUserID)
    }

    // 验证管理员用户是否存在
    if err := s.userClient.CheckUser(ctx, []string{req.UserID}); err != nil {
        return nil, err
    }

    // 生成管理员Token
    token, err := s.authDatabase.CreateToken(ctx, req.UserID, int(constant.AdminPlatformID))
    if err != nil {
        return nil, err
    }

    // 记录管理员登录指标
    prommetrics.UserLoginCounter.Inc()

    // 构建响应
    resp.Token = token
    resp.ExpireTimeSeconds = s.config.RpcConfig.TokenPolicy.Expire * 24 * 60 * 60
    return &resp, nil
}

安全验证层级:

  1. 系统密钥验证: 确保请求来自可信系统
  2. 管理员身份验证: 确认用户ID在管理员列表中
  3. 用户存在性验证: 确认管理员用户存在于系统中

5.5 第五阶段:用户ImToken生成

5.5.1 用户Token生成 - auth.go
// open-im-server/internal/rpc/auth/auth.go:267-303
func (s *authServer) GetUserToken(ctx context.Context, req *pbauth.GetUserTokenReq) (*pbauth.GetUserTokenResp, error) {
    // 验证调用者是否为管理员
    if err := authverify.CheckAdmin(ctx, s.config.Share.IMAdminUserID); err != nil {
        return nil, err
    }

    // 禁止为管理员平台生成Token
    if req.PlatformID == constant.AdminPlatformID {
        return nil, errs.ErrNoPermission.WrapMsg("platformID invalid. platformID must not be adminPlatformID")
    }

    resp := pbauth.GetUserTokenResp{}

    // 禁止为管理员用户生成普通Token
    if authverify.IsManagerUserID(req.UserID, s.config.Share.IMAdminUserID) {
        return nil, errs.ErrNoPermission.WrapMsg("don't get Admin token")
    }

    // 获取用户信息并验证
    user, err := s.userClient.GetUserInfo(ctx, req.UserID)
    if err != nil {
        return nil, err
    }

    // 应用级账号不能获取Token
    if user.AppMangerLevel >= constant.AppNotificationAdmin {
        return nil, errs.ErrArgs.WrapMsg("app account can`t get token")
    }

    // 生成用户Token
    token, err := s.authDatabase.CreateToken(ctx, req.UserID, int(req.PlatformID))
    if err != nil {
        return nil, err
    }

    resp.Token = token
    resp.ExpireTimeSeconds = s.config.RpcConfig.TokenPolicy.Expire * 24 * 60 * 60
    return &resp, nil
}

权限控制层级:

  1. 管理员权限验证: 只有管理员才能为用户生成Token
  2. 平台限制: 不能为管理员平台生成普通用户Token
  3. 用户类型检查: 防止权限提升攻击
  4. 应用账号保护: 特殊账号的额外保护
5.5.2 Token生成和缓存机制

Token的生成涉及复杂的多端登录策略和缓存机制(具体实现在controller.AuthDatabase中):

Token状态管理:

  • NormalToken(1): 正常有效状态
  • KickedToken(2): 被踢下线状态
  • 缓存键格式: UID_PID_TOKEN_STATUS:{userID}:{platformName}

多端登录策略:

  • DefaultNotKick: 允许多端同时在线
  • AllLoginButSameTermKick: 同终端互踢
  • AllLoginButSameClassKick: 同类别互踢

5.6 第六阶段:设备端Token保存

5.6.1 登录凭证缓存 - LoginCertificate.java
// open-im-android-demo2/OUIKit/OUICore/src/main/java/io/openim/android/ouicore/entity/LoginCertificate.java:32-40
public void cache(Context context) {
    Log.d(TAG, "LoginCertificate cache context:" + context);
    SharedPreferencesUtil.get(context).setCache("user.LoginCertificate",
        GsonHel.toJson(this));
}

public static LoginCertificate getCache(Context context) {
    String u = SharedPreferencesUtil.get(context).getString("user.LoginCertificate");
    if (u.isEmpty()) {
        Log.d(TAG, "LoginCertificate getCache null, context:" + context);
        return null;
    }
    Log.d(TAG, "LoginCertificate getCache ok. context:" + context);
    return GsonHel.fromJson(u, LoginCertificate.class);
}

LoginCertificate结构:

// open-im-android-demo2/OUIKit/OUICore/src/main/java/io/openim/android/ouicore/entity/LoginCertificate.java:13-26
public class LoginCertificate {
    public String nickname;      // 用户昵称
    public String faceURL;       // 头像URL
    public String userID;        // 用户ID
    public String imToken;       // IM连接Token
    public String chatToken;     // Chat API Token
    public boolean allowAddFriend;    // 允许添加好友
    public boolean allowBeep;         // 允许提示音
    public boolean allowVibration;    // 允许振动
    public int globalRecvMsgOpt;      // 全局消息接收选项
}
5.6.2 OpenIM SDK完整登录流程 - LoginVM.java

Chat系统认证成功后,Android应用需要调用OpenIM SDK的login接口建立IM连接:

// open-im-android-demo/Demo/app/src/main/java/io/openim/android/demo/vm/LoginVM.java:51-85
public void login(String verificationCode, int usedFor) {
    // 1. 保存登录类型到本地缓存
    SharedPreferencesUtil.get(BaseApp.inst())
        .setCache(Constants.K_LOGIN_TYPE, isPhone.val() ? 0 : 1);
    
    // 2. 构建登录参数并调用Chat系统API
    Parameter parameter = getParameter(verificationCode, usedFor);
    
    N.API(OpenIMService.class)
        .login(parameter.buildJsonBody())
        .compose(N.IOMain())
        .map(OpenIMService.turn(LoginCertificate.class))
        .subscribe(new NetObserver<LoginCertificate>(getContext()) {
            @Override
            public void onSuccess(LoginCertificate loginCertificate) {
                // 3. Chat登录成功后,立即调用OpenIM SDK登录
                try {
                    /**
                     * OpenIM SDK登录核心调用
                     * 参数说明:
                     * - callback: 登录结果回调接口
                     * - userID: 用户唯一标识,与Chat系统保持一致
                     * - imToken: IM连接Token,用于WebSocket认证
                     * 
                     * 功能作用:
                     * 1. 建立与OpenIM Server的WebSocket长连接
                     * 2. 初始化SDK内部各种管理器和服务
                     * 3. 启动消息同步、会话管理等后台服务
                     * 4. 验证Token并处理多端登录策略
                     */
                    OpenIMClient.getInstance().login(new OnBase<String>() {
                        @Override
                        public void onError(int code, String error) {
                            // SDK登录失败处理
                            log.ZError("SDK login failed", "code", code, "error", error);
                            getIView().err(error);
                        }
                        
                        @Override
                        public void onSuccess(String data) {
                            // 4. SDK登录成功,缓存完整登录信息
                            log.ZInfo("SDK login success", "userID", loginCertificate.userID);
                            
                            // 缓存登录凭证到本地存储
                            loginCertificate.cache(getContext());
                            
                            // 设置全局登录状态
                            BaseApp.inst().loginCertificate = loginCertificate;
                            
                            // 通知UI层登录成功
                            getIView().jump();
                        }
                    }, loginCertificate.userID, loginCertificate.imToken);
                    
                } catch (Exception e) {
                    log.ZError("SDK login exception", e);
                    getIView().err("SDK登录异常: " + e.getMessage());
                }
            }
            
            @Override
            protected void onFailure(Throwable e) {
                // Chat系统登录失败
                getIView().err(e.getMessage());
            }
        });
}

OpenIM SDK登录核心机制:

  1. 双层认证设计

    • Chat Token: 用于Chat系统REST API调用
    • IM Token: 用于OpenIM Server WebSocket连接
  2. SDK初始化流程

    // SDK内部初始化关键步骤
    OpenIMClient.getInstance().login() -> {
        // 1. 验证登录状态,防止重复登录
        // 2. 保存用户认证信息(userID + imToken)
        // 3. 初始化各种管理器:消息、会话、好友、群组等
        // 4. 启动后台服务:长连接、消息同步、事件处理
        // 5. 建立WebSocket连接到OpenIM Server
        // 6. 处理多端登录冲突和Token验证
    }
    
  3. 连接建立顺序

    Android应用 -> Chat系统认证 -> 获取双Token -> SDK初始化 -> WebSocket连接 -> 消息服务启动
    
  4. 错误处理机制

    • Chat认证失败: 直接返回错误,不进行SDK初始化
    • SDK初始化失败: 回滚登录状态,清理临时数据
    • WebSocket连接失败: 自动重连机制,支持断网恢复

5.7 第七阶段:openim-sdk-core登录

5.7.1 SDK初始化检查 - userRelated.go
// openim-sdk-core/open_im_sdk/userRelated.go:474-500
func (u *UserContext) login(ctx context.Context, userID, token string) error {
    // 1. 检查是否已经登录,避免重复登录
    if u.getLoginStatus(ctx) == Logged {
        return sdkerrs.ErrLoginRepeat
    }

    // 2. 设置登录状态为登录中
    u.setLoginStatus(Logging)
    log.ZDebug(ctx, "login start... ", "userID", userID, "token", token)
    t1 := time.Now() // 记录登录开始时间,用于统计登录耗时

    // 3. 保存用户认证信息
    u.info.UserID = userID
    u.info.Token = token

    // 4. 初始化各种模块和服务
    if err := u.initialize(ctx, userID); err != nil {
        return err
    }

    // 5. 启动各种后台服务和协程
    u.run(ctx)

    // 6. 设置登录状态为已登录
    u.setLoginStatus(Logged)
    log.ZDebug(ctx, "login success...", "login cost time: ", time.Since(t1))
    return nil
}
5.7.2 启动后台服务 - userRelated.go
// openim-sdk-core/open_im_sdk/userRelated.go:584-600
func (u *UserContext) run(ctx context.Context) {
    // 1. 启动长连接管理器(包含读写pump和心跳)
    u.longConnMgr.Run(ctx, u.fgCtx)

    // 2. 启动消息同步器监听协程
    go u.msgSyncer.DoListener(ctx)

    // 3. 启动会话事件处理协程
    go u.conversation.ConsumeConversationEventLoop(ctx)

    // 4. 启动登出监听协程
    go u.logoutListener(ctx)
}
5.7.3 长连接管理器启动 - long_conn_mgr.go
// openim-sdk-core/internal/interaction/long_conn_mgr.go:163-168
func (c *LongConnMgr) Run(ctx, fgCtx context.Context) {
    go c.readPump(ctx, fgCtx)  // 启动读消息协程
    go c.writePump(ctx)        // 启动写消息协程
    go c.heartbeat(ctx, fgCtx) // 启动心跳协程
}

关键协程说明:

  • readPump: 负责从WebSocket连接读取消息,处理重连逻辑
  • writePump: 负责向WebSocket连接发送消息,处理发送队列
  • heartbeat: 负责心跳检测,保持连接活跃状态
5.7.4 读消息协程启动与重连机制 - long_conn_mgr.go

readPump是长连接的核心协程,负责维护WebSocket连接和处理重连:

// openim-sdk-core/internal/interaction/long_conn_mgr.go:178-242
func (c *LongConnMgr) readPump(ctx context.Context, fgCtx context.Context) {
    // panic恢复机制,确保程序不会因为panic而崩溃
    defer func() {
        if r := recover(); r != nil {
            err := fmt.Sprintf("panic: %+v\n%s", r, debug.Stack())
            log.ZWarn(ctx, "readPump panic", nil, "panic info", err)
        }
    }()

    // 退出时清理资源
    defer func() {
        _ = c.close() // 关闭连接
        log.ZWarn(c.ctx, "readPump closed", c.closedErr)
    }()

    connNum := 0 // 连接计数器,用于统计重连次数

    // 主循环:持续读取消息
    for {
        // 检查上下文状态,支持优雅退出
        select {
        case <-ctx.Done():
            // 主上下文取消,通常是SDK退出登录
            c.closedErr = ctx.Err()
            log.ZInfo(c.ctx, "readPump done, sdk logout.....")
            return
        case <-fgCtx.Done():
            // 前台上下文取消,应用切换到后台
            c.closedErr = context.Cause(fgCtx)
            log.ZInfo(c.ctx, "SDK transitioning from foreground to background, read message goroutine ended.")
            return
        default:
            // 继续执行读取逻辑
        }

        // 为每次操作生成新的操作ID,便于日志追踪
        ctx = ccontext.WithOperationID(ctx, utils.OperationIDGenerator())

        // 尝试重连或确保连接可用 - 关键步骤
        needRecon, err := c.reConn(ctx, &connNum)
        if !needRecon {
            // 不需要重连,但有错误,说明是致命错误,需要退出
            c.closedErr = err
            return
        }
        if err != nil {
            // 重连失败,等待一段时间后重试
            log.ZWarn(c.ctx, "reConn", err)
            time.Sleep(c.reconnectStrategy.GetSleepInterval()) // 使用重连策略的等待间隔
            continue
        }

        // 连接建立成功,开始读取消息
        // 设置连接参数
        c.conn.SetReadLimit(maxMessageSize)  // 设置最大消息大小限制
        _ = c.conn.SetReadDeadline(pongWait) // 设置读取超时时间

        // 读取WebSocket消息并处理...
    }
}
5.7.5 WebSocket连接建立与Token携带 - long_conn_mgr.go

reConn方法是建立WebSocket连接的核心,这里展示了token如何通过URL参数传递给服务端:

// openim-sdk-core/internal/interaction/long_conn_mgr.go:938-970
func (c *LongConnMgr) reConn(ctx context.Context, num *int) (needRecon bool, err error) {
    // 检查连接状态,如果已连接则直接返回
    if c.IsConnected() {
        return true, nil
    }
    
    c.connWrite.Lock()         // 获取写锁,确保连接建立的原子性
    defer c.connWrite.Unlock() // 释放写锁
    
    // 通知监听器开始连接
    c.listener().OnConnecting()
    c.SetConnectionStatus(Connecting)
    
    // 构建WebSocket连接URL,关键:token通过URL参数传递
    url := fmt.Sprintf("%s?sendID=%s&token=%s&platformID=%d&operationID=%s&isBackground=%t",
        ccontext.Info(ctx).WsAddr(),      // WebSocket服务器地址
        ccontext.Info(ctx).UserID(),      // 用户ID
        ccontext.Info(ctx).Token(),       // 关键:ImToken通过URL参数传递
        ccontext.Info(ctx).PlatformID(),  // 平台ID
        ccontext.Info(ctx).OperationID(), // 操作ID,用于链路追踪
        c.GetBackground())                // 是否后台状态
    
    // 如果启用压缩,添加压缩参数
    if c.IsCompression {
        url += fmt.Sprintf("&compression=%s", "gzip")
    }
    
    log.ZDebug(ctx, "conn start", "url", url)
    
    // 发起WebSocket连接
    resp, err := c.conn.Dial(url, nil)
    if err != nil {
        c.SetConnectionStatus(Closed)
        
        // 处理连接失败响应
        if resp != nil {
            body, err := io.ReadAll(resp.Body)
            if err != nil {
                return true, err
            }
            log.ZInfo(ctx, "reConn resp", "body", string(body))
            
            // 解析错误响应
            var apiResp struct {
                ErrCode int    `json:"errCode"`
                ErrMsg  string `json:"errMsg"`
                ErrDlt  string `json:"errDlt"`
            }
            if err := json.Unmarshal(body, &apiResp); err != nil {
                return true, err
            }
            
            err = errs.NewCodeError(apiResp.ErrCode, apiResp.ErrMsg).WithDetail(apiResp.ErrDlt).Wrap()
            ccontext.GetApiErrCodeCallback(ctx).OnError(ctx, err)
            
            // 检查Token相关错误,这些错误不需要重连
            switch apiResp.ErrCode {
            case
                errs.TokenExpiredError,      // Token过期
                errs.TokenInvalidError,      // Token无效
                errs.TokenMalformedError,    // Token格式错误
                errs.TokenNotValidYetError,  // Token尚未生效
                errs.TokenUnknownError,      // Token未知错误
                errs.TokenNotExistError,     // Token不存在
                errs.TokenKickedError:       // Token被踢下线
                return false, err // 不需要重连,直接返回错误
            default:
                return true, err // 其他错误,可以重连
            }
        }
        
        c.listener().OnConnectFailed(sdkerrs.NetworkError, err.Error())
        return true, err
    }
    
    // 连接建立成功后的处理
    if err := c.writeConnFirstSubMsg(ctx); err != nil {
        log.ZError(ctx, "first write user online sub info error", err)
        ccontext.GetApiErrCodeCallback(ctx).OnError(ctx, err)
        c.listener().OnConnectFailed(sdkerrs.NetworkError, err.Error())
        c.conn.Close()
        return true, err
    }
    
    // 通知连接成功
    c.listener().OnConnectSuccess()
    c.sub.onConnSuccess()
    c.ctx = newContext(c.conn.LocalAddr())
    c.ctx = context.WithValue(ctx, "ConnContext", c.ctx)
    c.SetConnectionStatus(Connected)
    
    // 设置心跳处理器
    c.conn.SetPongHandler(c.pongHandler)
    c.conn.SetPingHandler(c.pingHandler)
    
    *num++
    log.ZInfo(c.ctx, "long conn establish success", "localAddr", c.conn.LocalAddr(), "connNum", *num)
    
    // 重置重连策略
    c.reconnectStrategy.Reset()
    
    // 通知消息同步器连接已建立
    _ = common.DispatchConnected(ctx, c.pushMsgAndMaxSeqCh)
    return true, nil
}

WebSocket URL构建关键点:

  1. Token传递方式: 通过URL查询参数token字段携带ImToken

  2. 必要参数:

    • sendID: 用户ID,标识连接的所有者
    • token: ImToken,用于身份验证(阶段5生成的)
    • platformID: 平台ID,区分不同设备类型
    • operationID: 操作ID,用于链路追踪
    • isBackground: 后台状态标识
    • compression: 压缩类型(可选)
  3. 错误处理: 详细的Token相关错误处理,区分可重连和不可重连的错误

连接URL示例:

ws://localhost:10003?sendID=user123&token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...&platformID=2&operationID=1640123456789&isBackground=false&compression=gzip

5.8 第八阶段:SDK HTTP请求Token认证与API网关验证

登录成功后,SDK在进行HTTP API调用时需要携带Token进行身份认证,OpenIM Server的API网关会对这些请求进行统一的Token验证。

5.8.1 SDK HTTP客户端Token携带 - http_client.go

SDK的HTTP客户端在发送API请求时会自动携带Token:

// openim-sdk-core/pkg/network/http_client.go:90-110
func ApiPost(ctx context.Context, api string, req, resp any) (err error) {
    // 提取operationID并验证
    operationID, _ := ctx.Value("operationID").(string)
    if operationID == "" {
        err := sdkerrs.ErrArgs.WrapMsg("call api operationID is empty")
        log.ZError(ctx, "ApiRequest", err, "type", "ctx not set operationID")
        return err
    }

    // 序列化请求对象为JSON
    reqBody, err := json.Marshal(req)
    if err != nil {
        log.ZError(ctx, "ApiRequest", err, "type", "json.Marshal(req) failed")
        return sdkerrs.ErrSdkInternal.WrapMsg("json.Marshal(req) failed " + err.Error())
    }

    // 构建完整的API URL并创建HTTP请求
    ctxInfo := ccontext.Info(ctx)
    reqUrl := ctxInfo.ApiAddr() + api
    request, err := http.NewRequestWithContext(ctx, http.MethodPost, reqUrl, bytes.NewReader(reqBody))
    if err != nil {
        log.ZError(ctx, "ApiRequest", err, "type", "http.NewRequestWithContext failed")
        return sdkerrs.ErrSdkInternal.WrapMsg("sdk http.NewRequestWithContext failed " + err.Error())
    }

    // 设置关键的请求头信息
    log.ZDebug(ctx, "ApiRequest", "url", reqUrl, "token", ctxInfo.Token(), "body", string(reqBody))
    request.ContentLength = int64(len(reqBody))
    request.Header.Set("Content-Type", "application/json")
    request.Header.Set("operationID", operationID)
    request.Header.Set("token", ctxInfo.Token())           // 关键:携带认证Token
    request.Header.Set("Accept-Encoding", "gzip")

    // 发送请求并处理响应...
}

关键点分析:

  • Token来源: 从上下文信息中获取登录时缓存的ImToken
  • 请求头设置: 通过token请求头携带认证信息
  • 链路追踪: 通过operationID实现请求的全链路追踪
  • 内容协商: 支持gzip压缩以优化传输效率
5.8.2 API网关统一Token验证 - router.go

OpenIM Server的API网关对所有API请求进行统一的Token验证:

// open-im-server/internal/api/router.go:130-153
func newGinRouter(ctx context.Context, client discovery.SvcDiscoveryRegistry, config *Config) (*gin.Engine, error) {
    // 建立各种RPC连接...
    authConn, err := client.GetConn(ctx, config.Share.RpcRegisterName.Auth)
    if err != nil {
        return nil, err
    }
    
    // 初始化Gin路由器
    gin.SetMode(gin.ReleaseMode)
    r := gin.New()
    
    // 注册全局中间件,包括Token解析中间件
    r.Use(prommetricsGin(), gin.RecoveryWithWriter(gin.DefaultErrorWriter, mw.GinPanicErr), 
          mw.CorsHandler(), mw.GinParseOperationID(), 
          GinParseToken(rpcli.NewAuthClient(authConn))) // 核心:Token解析中间件
    
    // 注册各种业务路由...
    return r, nil
}

// GinParseToken Token解析中间件实现
func GinParseToken(authClient *rpcli.AuthClient) gin.HandlerFunc {
    return func(c *gin.Context) {
        switch c.Request.Method {
        case http.MethodPost:
            // 1. 检查API白名单,部分接口无需Token验证
            for _, wApi := range Whitelist {
                if strings.HasPrefix(c.Request.URL.Path, wApi) {
                    c.Next()  // 白名单接口直接放行
                    return
                }
            }

            // 2. 从请求头中提取Token
            token := c.Request.Header.Get(constant.Token)
            if token == "" {
                log.ZWarn(c, "header get token error", servererrs.ErrArgs.WrapMsg("header must have token"))
                apiresp.GinError(c, servererrs.ErrArgs.WrapMsg("header must have token"))
                c.Abort()  // 终止请求处理
                return
            }

            // 3. 调用认证服务解析Token
            resp, err := authClient.ParseToken(c, token)
            if err != nil {
                apiresp.GinError(c, err)
                c.Abort()  // Token验证失败,终止请求
                return
            }

            // 4. 将用户信息设置到请求上下文中
            c.Set(constant.OpUserPlatform, constant.PlatformIDToName(int(resp.PlatformID)))
            c.Set(constant.OpUserID, resp.UserID)
            c.Next()  // 继续处理请求
        }
        kickTokenFunc(kickClients)
    }
}
5.8.3 认证服务Token解析实现 - auth.go

认证服务提供Token解析的具体实现:

// open-im-server/internal/rpc/auth/auth.go:354-369
func (s *authServer) ParseToken(ctx context.Context, req *pbauth.ParseTokenReq) (resp *pbauth.ParseTokenResp, error) {
    resp = &pbauth.ParseTokenResp{}

    // 调用内部Token解析方法
    claims, err := s.parseToken(ctx, req.Token)
    if err != nil {
        return nil, err
    }

    // 构建响应,返回Token中的用户信息
    resp.UserID = claims.UserID                    // 用户ID
    resp.PlatformID = int32(claims.PlatformID)     // 平台ID
    resp.ExpireTimeSeconds = claims.ExpiresAt.Unix() // Token过期时间
    return resp, nil
}

// parseToken Token解析的核心逻辑
func (s *authServer) parseToken(ctx context.Context, tokensString string) (claims *tokenverify.Claims, err error) {
    // 1. 解析JWT Token,验证签名和格式
    claims, err = tokenverify.GetClaimFromToken(tokensString, authverify.Secret(s.config.Share.Secret))
    if err != nil {
        return nil, err
    }

    // 2. 检查是否为管理员Token(管理员Token享有特殊权限)
    isAdmin := authverify.IsManagerUserID(claims.UserID, s.config.Share.IMAdminUserID)
    if isAdmin {
        return claims, nil // 管理员Token直接通过,跳过Redis状态检查
    }

    // 3. 从Redis获取普通用户Token的状态映射
    m, err := s.authDatabase.GetTokensWithoutError(ctx, claims.UserID, claims.PlatformID)
    if err != nil {
        return nil, err
    }

    // 4. Token不存在于Redis中(可能已过期或被删除)
    if len(m) == 0 {
        return nil, servererrs.ErrTokenNotExist.Wrap()
    }

    // 5. 检查特定Token的状态
    if v, ok := m[tokensString]; ok {
        switch v {
        case constant.NormalToken:
            return claims, nil // Token状态正常
        case constant.KickedToken:
            return nil, servererrs.ErrTokenKicked.Wrap() // Token已被踢下线
        default:
            return nil, errs.Wrap(errs.ErrTokenUnknown) // 未知Token状态
        }
    }

    return nil, servererrs.ErrTokenNotExist.Wrap()
}

Token验证安全机制:

  1. JWT签名验证: 使用系统密钥验证Token的完整性和真实性
  2. 管理员特权: 系统管理员Token跳过Redis状态检查,提高性能
  3. 状态实时检查: 从Redis查询Token的实时状态,支持踢人等操作
  4. 多状态支持: 区分正常Token、被踢Token和未知状态
  5. 过期自动清理: 过期Token从Redis中自动清理
5.8.4 认证客户端RPC调用 - auth.go

API网关通过RPC客户端调用认证服务:

// open-im-server/pkg/rpcli/auth.go:25-27
func (x *AuthClient) ParseToken(ctx context.Context, token string) (*auth.ParseTokenResp, error) {
    return x.AuthClient.ParseToken(ctx, &auth.ParseTokenReq{Token: token})
}

RPC调用特点:

  • 简化接口: 封装了gRPC调用的复杂性
  • 类型安全: 强类型的请求和响应结构
  • 错误传播: 透明的错误传播机制
  • 性能优化: 连接复用和负载均衡

5.9 第九阶段:WebSocket连接建立与多端登录处理

5.9.1 WebSocket连接处理 - ws_server.go
// open-im-server/internal/msggateway/ws_server.go:768-834
func (ws *WsServer) wsHandler(w http.ResponseWriter, r *http.Request) {
    // 创建连接上下文
    connContext := newContext(w, r)

    // 检查连接数限制
    if ws.onlineUserConnNum.Load() >= ws.wsMaxConnNum {
        httpError(connContext, servererrs.ErrConnOverMaxNumLimit.WrapMsg("over max conn num limit"))
        return
    }

    // 解析必要参数(用户ID、令牌等)
    err := connContext.ParseEssentialArgs()
    if err != nil {
        httpError(connContext, err)
        return
    }

    // 调用认证服务解析令牌
    resp, err := ws.authClient.ParseToken(connContext, connContext.GetToken())
    if err != nil {
        // 根据上下文判断是否需要通过WebSocket发送错误
        shouldSendError := connContext.ShouldSendResp()
        if shouldSendError {
            // 尝试建立WebSocket连接发送错误
            wsLongConn := newGWebSocket(WebSocket, ws.handshakeTimeout, ws.writeBufferSize)
            if err := wsLongConn.RespondWithError(err, w, r); err == nil {
                return
            }
        }
        // 通过HTTP返回错误
        httpError(connContext, err)
        return
    }

    // 验证认证响应的匹配性
    err = ws.validateRespWithRequest(connContext, resp)
    if err != nil {
        httpError(connContext, err)
        return
    }

    log.ZDebug(connContext, "new conn", "token", connContext.GetToken())

    // 创建WebSocket长连接
    wsLongConn := newGWebSocket(WebSocket, ws.handshakeTimeout, ws.writeBufferSize)
    if err := wsLongConn.GenerateLongConn(w, r); err != nil {
        log.ZWarn(connContext, "long connection fails", err)
        return
    } else {
        // 检查是否需要发送成功响应
        shouldSendSuccessResp := connContext.ShouldSendResp()
        if shouldSendSuccessResp {
            if err := wsLongConn.RespondWithSuccess(); err != nil {
                return
            }
        }
    }

    // 从对象池获取客户端对象并重置状态
    client := ws.clientPool.Get().(*Client)
    client.ResetClient(connContext, wsLongConn, ws)

    // 注册客户端并启动消息处理循环
    ws.registerChan <- client
    go client.readMessage()
}
5.9.2 Token验证逻辑 - auth.go
// open-im-server/internal/rpc/auth/auth.go:312-346
func (s *authServer) parseToken(ctx context.Context, tokensString string) (claims *tokenverify.Claims, err error) {
    // 解析JWT Token
    claims, err = tokenverify.GetClaimFromToken(tokensString, authverify.Secret(s.config.Share.Secret))
    if err != nil {
        return nil, err
    }

    // 检查是否为管理员Token
    isAdmin := authverify.IsManagerUserID(claims.UserID, s.config.Share.IMAdminUserID)
    if isAdmin {
        return claims, nil // 管理员Token直接通过,跳过Redis状态检查
    }

    // 从Redis获取Token状态映射
    m, err := s.authDatabase.GetTokensWithoutError(ctx, claims.UserID, claims.PlatformID)
    if err != nil {
        return nil, err
    }

    // Token不存在于Redis中
    if len(m) == 0 {
        return nil, servererrs.ErrTokenNotExist.Wrap()
    }

    // 检查特定Token的状态
    if v, ok := m[tokensString]; ok {
        switch v {
        case constant.NormalToken:
            return claims, nil
        case constant.KickedToken:
            return nil, servererrs.ErrTokenKicked.Wrap()
        default:
            return nil, errs.Wrap(errs.ErrTokenUnknown)
        }
    }

    return nil, servererrs.ErrTokenNotExist.Wrap()
}

Token验证流程:

  1. JWT解析: 验证Token格式和签名
  2. 管理员特权: 管理员Token跳过Redis检查
  3. 状态查询: 从Redis查询Token状态
  4. 状态验证: 检查Token是否正常、被踢或过期
5.9.3 多端登录策略处理核心机制 - ws_server.go
// open-im-server/internal/msggateway/ws_server.go:635-688
func (ws *WsServer) multiTerminalLoginChecker(clientOK bool, oldClients []*Client, newClient *Client) {
    // 根据多端登录策略执行相应逻辑
    switch ws.msgGatewayConfig.Share.MultiLogin.Policy {
    case constant.DefalutNotKick:
        // 默认策略:不踢任何连接

    case constant.PCAndOther:
        // PC和其他策略:PC端不踢,其他端按终端处理
        if constant.PlatformIDToClass(newClient.PlatformID) == constant.TerminalPC {
            return
        }
        fallthrough

    case constant.AllLoginButSameTermKick:
        // 同终端踢下线策略
        if !clientOK {
            return
        }

        // 删除旧连接
        ws.clients.DeleteClients(newClient.UserID, oldClients)
        for _, c := range oldClients {
            err := c.KickOnlineMessage()
            if err != nil {
                log.ZWarn(c.ctx, "KickOnlineMessage", err)
            }
        }

        // 失效旧令牌,保留新令牌
        ctx := mcontext.WithMustInfoCtx(
            []string{newClient.ctx.GetOperationID(), newClient.ctx.GetUserID(),
                constant.PlatformIDToName(newClient.PlatformID), newClient.ctx.GetConnID()},
        )
        req := &pbAuth.InvalidateTokenReq{
            PreservedToken: newClient.token,
            UserID:         newClient.UserID,
            PlatformID:     int32(newClient.PlatformID),
        }
        if err := ws.authClient.InvalidateToken(ctx, req); err != nil {
            log.ZWarn(newClient.ctx, "InvalidateToken err", err, "userID", newClient.UserID,
                "platformID", newClient.PlatformID)
        }

    case constant.AllLoginButSameClassKick:
        // 同类别踢下线策略
        clients, ok := ws.clients.GetAll(newClient.UserID)
        if !ok {
            return
        }

        var kickClients []*Client
        // 查找同类别的连接
        for _, client := range clients {
            if constant.PlatformIDToClass(client.PlatformID) == constant.PlatformIDToClass(newClient.PlatformID) {
                kickClients = append(kickClients, client)
            }
        }
        kickTokenFunc(kickClients)
    }
}
5.9.4 多端登录剔除机制深度解析

剔除策略详细说明:

  1. DefalutNotKick(默认不踢)

    • 允许所有平台同时在线
    • 适用于宽松的多端使用场景
    • 不进行任何连接冲突处理
  2. PCAndOther(PC和其他)

    • PC端享有特殊权限,不被踢下线
    • 其他平台按照同终端踢下线策略处理
    • 体现PC端的重要性和稳定性需求
  3. AllLoginButSameTermKick(同终端踢下线)

    // 核心踢下线流程
    func handleSameTerminalKick(oldClients []*Client, newClient *Client) {
        // 1. 从用户映射中删除旧连接
        ws.clients.DeleteClients(newClient.UserID, oldClients)
        
        // 2. 向旧连接发送踢下线消息
        for _, c := range oldClients {
            err := c.KickOnlineMessage()
            if err != nil {
                log.ZWarn(c.ctx, "KickOnlineMessage", err)
            }
        }
        
        // 3. 调用认证服务失效旧Token,保留新Token
        ctx := mcontext.WithMustInfoCtx(...)
        req := &pbAuth.InvalidateTokenReq{
            PreservedToken: newClient.token,  // 保留新连接的Token
            UserID:         newClient.UserID,
            PlatformID:     int32(newClient.PlatformID),
        }
        ws.authClient.InvalidateToken(ctx, req)
    }
    

5.10. OpenIM登录流程完整时序图

以下时序图展示了OpenIM设备登录的完整流程,包括Chat系统认证、Token生成、SDK初始化和WebSocket连接建立:

sequenceDiagram
    participant Client as Android Demo<br/>LoginActivity
    participant SDK as open-im-sdk-android<br/>LoginVM
    participant ChatAPI as Chat System<br/>HTTP API
    participant ChatRPC as Chat System<br/>RPC Service
    participant Redis as Redis Cache<br/>Token/Session存储
    participant ChatDB as Chat Database<br/>MongoDB
    participant OpenIMAPI as OpenIM Server<br/>RPC API
    participant OpenIMDB as OpenIM Database<br/>MongoDB
    participant IMSDK as openim-sdk-core<br/>UserContext
    participant WSGateway as OpenIM Gateway<br/>WebSocket Server
    participant UserMap as Connection Map<br/>用户连接管理

    Note over Client,UserMap: === 阶段1: 用户登录界面交互 ===
    
    Client->>SDK: 1.1 用户输入登录信息<br/>账号/手机/邮箱 + 密码/验证码
    SDK->>SDK: 1.2 客户端参数验证<br/>格式检查、必填项验证
    SDK->>SDK: 1.3 构建登录参数<br/>密码MD5加密、平台标识等

    Note over Client,UserMap: === 阶段2: Chat系统认证 ===
    
    SDK->>ChatAPI: 2.1 POST /account/login<br/>{"phoneNumber":"...", "password":"...", "platform":2}
    ChatAPI->>ChatAPI: 2.2 获取客户端IP<br/>用于安全检查和登录记录
    ChatAPI->>ChatRPC: 2.3 Login RPC调用<br/>传递完整登录信息
    
    ChatRPC->>ChatRPC: 2.4 参数验证<br/>密码和验证码至少一个
    ChatRPC->>ChatRPC: 2.5 构建账号凭证<br/>手机号: +86 13800000000
    ChatRPC->>ChatDB: 2.6 查询用户凭证<br/>根据账号查找用户信息
    ChatDB-->>ChatRPC: 2.7 返回用户凭证信息
    
    ChatRPC->>ChatRPC: 2.8 IP和用户登录频率检查<br/>防止暴力破解
    
    alt 密码登录
        ChatRPC->>ChatDB: 2.9a 查询账户密码
        ChatDB-->>ChatRPC: 2.10a 返回密码哈希
        ChatRPC->>ChatRPC: 2.11a 密码MD5比对验证
    else 验证码登录
        ChatRPC->>Redis: 2.9b 验证验证码<br/>Key: verify_code:{account}:3
        Redis-->>ChatRPC: 2.10b 返回存储的验证码
        ChatRPC->>ChatRPC: 2.11b 验证码比对验证
        ChatRPC->>Redis: 2.12b 删除已使用验证码
    end
    
    ChatRPC->>ChatRPC: 2.13 生成ChatToken<br/>JWT: {userID, userType:1, platformID:0}
    ChatRPC->>Redis: 2.14 缓存ChatToken状态<br/>Key: CHAT_UID_TOKEN_STATUS:{userID}
    ChatRPC->>ChatDB: 2.15 记录登录日志<br/>IP、设备ID、平台信息
    ChatRPC-->>ChatAPI: 2.16 返回认证结果<br/>{userID, chatToken}

    Note over Client,UserMap: === 阶段3: 获取OpenIM管理员Token ===
    
    ChatAPI->>ChatAPI: 3.1 获取管理员Token<br/>ImAdminTokenWithDefaultAdmin()
    ChatAPI->>OpenIMAPI: 3.2 GetAdminToken RPC<br/>{secret, adminUserID}
    OpenIMAPI->>OpenIMAPI: 3.3 验证系统密钥<br/>config.Share.Secret
    OpenIMAPI->>OpenIMAPI: 3.4 验证管理员身份<br/>IMAdminUserID列表检查
    OpenIMAPI->>OpenIMDB: 3.5 检查管理员用户存在性
    OpenIMDB-->>OpenIMAPI: 3.6 确认用户存在
    OpenIMAPI->>OpenIMAPI: 3.7 生成管理员Token<br/>JWT: {adminUserID, AdminPlatformID}
    OpenIMAPI->>Redis: 3.8 缓存管理员Token<br/>Key: CHAT:IM_TOKEN:{adminUserID}
    OpenIMAPI-->>ChatAPI: 3.9 返回管理员Token

    Note over Client,UserMap: === 阶段4: 生成用户ImToken ===
    
    ChatAPI->>OpenIMAPI: 4.1 GetUserToken RPC<br/>{userID, platformID} + AdminToken
    OpenIMAPI->>OpenIMAPI: 4.2 验证管理员权限<br/>CheckAdmin(adminUserID)
    OpenIMAPI->>OpenIMAPI: 4.3 平台ID合法性检查<br/>不能为AdminPlatformID
    OpenIMAPI->>OpenIMDB: 4.4 获取用户信息验证<br/>检查用户存在和权限
    OpenIMDB-->>OpenIMAPI: 4.5 返回用户信息
    OpenIMAPI->>OpenIMAPI: 4.6 生成用户ImToken<br/>JWT: {userID, platformID}
    OpenIMAPI->>Redis: 4.7 缓存ImToken状态<br/>Key: UID_PID_TOKEN_STATUS:{userID}:Android
    OpenIMAPI-->>ChatAPI: 4.8 返回用户ImToken
    ChatAPI-->>SDK: 4.9 返回登录结果<br/>{userID, chatToken, imToken}

    Note over Client,UserMap: === 阶段5: Android SDK初始化 ===
    
    SDK->>SDK: 5.1 缓存登录凭证<br/>LoginCertificate.cache()
    SDK->>IMSDK: 5.2 调用OpenIM SDK登录<br/>OpenIMClient.getInstance().login()
    IMSDK->>IMSDK: 5.3 检查登录状态<br/>避免重复登录
    IMSDK->>IMSDK: 5.4 设置登录状态为Logging<br/>保存userID和token
    IMSDK->>IMSDK: 5.5 初始化各种管理器<br/>消息、会话、好友、群组等
    IMSDK->>IMSDK: 5.6 启动后台服务<br/>长连接、消息同步、事件处理

    Note over Client,UserMap: === 阶段6: WebSocket连接建立 ===
    
    IMSDK->>IMSDK: 6.1 启动长连接管理器<br/>readPump, writePump, heartbeat
    IMSDK->>IMSDK: 6.2 构建连接URL<br/>携带token等参数
    IMSDK->>WSGateway: 6.3 WebSocket连接请求<br/>ws://server?sendID={userID}&token={imToken}&platformID=2
    
    WSGateway->>WSGateway: 6.4 检查连接数限制<br/>防止过载
    WSGateway->>WSGateway: 6.5 解析连接参数<br/>userID, token, platformID等
    WSGateway->>OpenIMAPI: 6.6 ParseToken RPC调用<br/>验证ImToken
    
    OpenIMAPI->>OpenIMAPI: 6.7 JWT Token解析<br/>验证签名和格式
    alt 管理员Token
        OpenIMAPI->>OpenIMAPI: 6.8a 管理员特权处理<br/>跳过Redis状态检查
    else 普通用户Token
        OpenIMAPI->>Redis: 6.8b 查询Token状态<br/>Key: UID_PID_TOKEN_STATUS:{userID}:Android
        Redis-->>OpenIMAPI: 6.9b 返回Token状态
        OpenIMAPI->>OpenIMAPI: 6.10b 检查Token状态<br/>Normal/Kicked/Unknown
    end
    
    OpenIMAPI-->>WSGateway: 6.11 返回解析结果<br/>{userID, platformID, expireTime}
    WSGateway->>WSGateway: 6.12 验证请求匹配性<br/>URL参数与Token一致性
    WSGateway->>WSGateway: 6.13 建立WebSocket连接<br/>协议升级
    WSGateway->>WSGateway: 6.14 创建Client对象<br/>从对象pool获取并重置

    Note over Client,UserMap: === 阶段7: 多端登录处理 ===
    
    WSGateway->>UserMap: 7.1 检查用户连接状态<br/>Get(userID, platformID)
    UserMap-->>WSGateway: 7.2 返回连接状态<br/>userOK, clientOK, oldClients
    
    alt 用户首次登录
        WSGateway->>UserMap: 7.3a 注册新用户连接<br/>Set(userID, client)
        WSGateway->>WSGateway: 7.4a 更新监控指标<br/>onlineUserNum++
    else 用户已存在连接
        WSGateway->>WSGateway: 7.3b 多端登录策略检查<br/>根据配置处理冲突
        
        alt 同终端踢下线策略
            WSGateway->>UserMap: 7.4b1 删除旧连接<br/>DeleteClients(userID, oldClients)
            WSGateway->>WSGateway: 7.5b1 发送踢下线消息<br/>KickOnlineMessage()
            WSGateway->>OpenIMAPI: 7.6b1 失效旧Token<br/>InvalidateToken RPC
            OpenIMAPI->>Redis: 7.7b1 更新Token状态<br/>设置为KickedToken
        else 允许多端登录
            WSGateway->>UserMap: 7.4b2 正常添加连接<br/>Set(userID, client)
        end
    end

    Note over Client,UserMap: === 阶段8: 连接成功与服务启动 ===
    
    WSGateway->>WSGateway: 8.1 启动消息处理协程<br/>client.readMessage()
    WSGateway->>IMSDK: 8.2 连接成功通知<br/>OnConnectSuccess回调
    IMSDK->>IMSDK: 8.3 设置登录状态为Logged<br/>初始化完成
    IMSDK->>IMSDK: 8.4 启动消息同步服务<br/>msgSyncer.DoListener()
    IMSDK->>IMSDK: 8.5 启动会话事件处理<br/>ConsumeConversationEventLoop()
    IMSDK->>SDK: 8.6 SDK登录成功回调<br/>onSuccess(data)
    SDK->>SDK: 8.7 设置全局登录状态<br/>BaseApp.loginCertificate
    SDK->>Client: 8.8 通知UI登录成功<br/>jump()跳转主界面

    Note over Client,UserMap: === 数据存储结构说明 ===
    Note over Redis: ChatToken: CHAT_UID_TOKEN_STATUS:{userID}<br/>AdminToken: CHAT:IM_TOKEN:{adminUserID}<br/>ImToken: UID_PID_TOKEN_STATUS:{userID}:{platform}<br/>验证码: verify_code:{account}:{usedFor}
    Note over UserMap: 用户连接映射: {userID -> UserPlatform}<br/>连接状态通知: UserState Channel<br/>多端登录冲突处理: 实时剔除机制