跳转到正文

后端认证示例 ​

这一页实现认证与登录中的后端步骤:你的后端用 SDKSecret 调用 Popo IM,为已经登录你产品的当前用户获取 IM Token。选择你的后端语言即可,客户端 SDK 的接入语言不影响这里的选择。

这里的认证流程是携带密钥换 Token,无需自行计算签名。 JWT 的 HS256 签发与验签由 Popo IM 完成,算法说明见认证和签名算法。

先准备这两项配置 ​

在业务后端的运行环境中设置:

环境变量内容
POPOIM_SERVER_URLPopo IM 部署根地址,例如 https://im.example.com
POPOIM_SDK_SECRETPopo IM 安装生成的 SDKSecret,见获取方式与修改步骤

生产使用 HTTPS,并保留 HTTP 客户端的证书校验。下面的函数均设置 10 秒请求超时、不跟随重定向,并校验 HTTP 状态、业务 code、用户身份、Token 及到期时间。

选择后端语言 ​

示例是可放入你项目的取 Token 函数,没有替你实现宿主登录系统。传入的 user 必须由登录中间件确认身份后,从服务端资料库构造,字段如下:

json
{
  "user_id": "chenyue",
  "nickname": "陈悦",
  "username": "chenyue",
  "avatar": "https://example.com/media/chenyue.webp",
  "signature": "记录每一次出发"
}

user_id、nickname 必填;其余字段应按当前完整资料提交,省略会重置相应值。不要将客户端请求 Body 直接传给此函数。

php
<?php
// PHP 8+,启用 cURL 扩展。
function fetchImAuth(array $user): array
{
    $server = rtrim((string) getenv('POPOIM_SERVER_URL'), '/');
    $secret = (string) getenv('POPOIM_SDK_SECRET');
    if ($server === '' || $secret === '') {
        throw new RuntimeException('请配置 Popo IM 地址和 SDKSecret');
    }

    $curl = curl_init($server . '/app/sdk/token');
    curl_setopt_array($curl, [
        CURLOPT_POST => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => false,
        CURLOPT_CONNECTTIMEOUT => 3,
        CURLOPT_TIMEOUT => 10,
        CURLOPT_HTTPHEADER => [
            'Authorization: Bearer ' . $secret,
            'Content-Type: application/json',
        ],
        CURLOPT_POSTFIELDS => json_encode($user, JSON_THROW_ON_ERROR),
    ]);
    $body = curl_exec($curl);
    $status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
    curl_close($curl);
    if ($body === false || $status < 200 || $status >= 300) {
        throw new RuntimeException('IM 取 Token 请求失败');
    }

    $result = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
    if (($result['code'] ?? null) !== 0) {
        throw new RuntimeException('IM 取 Token 业务失败');
    }
    $data = $result['data'] ?? null;
    if (!is_array($data)
        || !is_array($data['user'] ?? null)
        || ($data['user']['user_id'] ?? null) !== $user['user_id']
        || !is_string($data['token'] ?? null) || $data['token'] === ''
        || !is_int($data['expire_at'] ?? null) || $data['expire_at'] <= time()) {
        throw new RuntimeException('IM 认证结果无效');
    }
    return $data;
}
java
// Java 17+;JSON 使用项目中的 Jackson Databind 2.x:
// com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.net.URI;
import java.net.http.*;
import java.time.Duration;
import java.time.Instant;
import java.util.Map;

public final class ImAuth {
    private static final ObjectMapper JSON = new ObjectMapper();
    private static final HttpClient HTTP = HttpClient.newBuilder()
        .connectTimeout(Duration.ofSeconds(3))
        .followRedirects(HttpClient.Redirect.NEVER).build();

    public static JsonNode fetchImAuth(Map<String, String> user) throws Exception {
        String server = System.getenv("POPOIM_SERVER_URL");
        String secret = System.getenv("POPOIM_SDK_SECRET");
        if (server == null || server.isBlank() || secret == null || secret.isBlank()) {
            throw new IllegalStateException("请配置 Popo IM 地址和 SDKSecret");
        }
        var request = HttpRequest.newBuilder(
                URI.create(server.replaceAll("/+$", "") + "/app/sdk/token"))
            .timeout(Duration.ofSeconds(10))
            .header("Authorization", "Bearer " + secret)
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(JSON.writeValueAsString(user)))
            .build();
        var response = HTTP.send(request, HttpResponse.BodyHandlers.ofString());
        if (response.statusCode() < 200 || response.statusCode() >= 300) {
            throw new IOException("IM 取 Token 请求失败");
        }
        JsonNode result = JSON.readTree(response.body());
        if (result == null || !result.path("code").isIntegralNumber()
            || result.path("code").asLong() != 0) {
            throw new IOException("IM 取 Token 业务失败");
        }
        JsonNode data = result.path("data");
        if (!data.isObject()
            || !data.path("user").path("user_id").isTextual()
            || !data.path("user").path("user_id").asText().equals(user.get("user_id"))
            || !data.path("token").isTextual() || data.path("token").asText().isBlank()
            || !data.path("expire_at").isIntegralNumber()
            || data.path("expire_at").asLong() <= Instant.now().getEpochSecond()) {
            throw new IOException("IM 认证结果无效");
        }
        return data;
    }
}
go
// Go 1.20+,仅使用标准库。
package imauth

import (
    "bytes"
    "encoding/json"
    "errors"
    "net/http"
    "os"
    "strings"
    "time"
)

var client = &http.Client{
    Timeout: 10 * time.Second,
    CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
        return http.ErrUseLastResponse
    },
}

func FetchImAuth(user map[string]string) (json.RawMessage, error) {
    server := strings.TrimRight(os.Getenv("POPOIM_SERVER_URL"), "/")
    secret := os.Getenv("POPOIM_SDK_SECRET")
    if server == "" || secret == "" {
        return nil, errors.New("请配置 Popo IM 地址和 SDKSecret")
    }
    body, err := json.Marshal(user)
    if err != nil { return nil, err }
    req, err := http.NewRequest(http.MethodPost, server+"/app/sdk/token", bytes.NewReader(body))
    if err != nil { return nil, err }
    req.Header.Set("Authorization", "Bearer "+secret)
    req.Header.Set("Content-Type", "application/json")
    resp, err := client.Do(req)
    if err != nil { return nil, errors.New("IM 取 Token 请求失败") }
    defer resp.Body.Close()
    if resp.StatusCode < 200 || resp.StatusCode >= 300 {
        return nil, errors.New("IM 取 Token 请求失败")
    }
    var result struct {
        Code *int `json:"code"`
        Data json.RawMessage `json:"data"`
    }
    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, err }
    if result.Code == nil || *result.Code != 0 {
        return nil, errors.New("IM 取 Token 业务失败")
    }
    var data struct {
        User struct { UserID string `json:"user_id"` } `json:"user"`
        Token string `json:"token"`
        ExpireAt int64 `json:"expire_at"`
    }
    if err := json.Unmarshal(result.Data, &data); err != nil { return nil, err }
    if data.User.UserID == "" || data.User.UserID != user["user_id"] ||
        strings.TrimSpace(data.Token) == "" || data.ExpireAt <= time.Now().Unix() {
        return nil, errors.New("IM 认证结果无效")
    }
    // 返回原始 data,保留昵称、头像等字段,而非只返回上面校验用的字段。
    return result.Data, nil
}
js
// Node.js 22+,使用内置 fetch;此文件只在后端运行。
export async function fetchImAuth(user) {
  const server = process.env.POPOIM_SERVER_URL?.replace(/\/+$/, '')
  const secret = process.env.POPOIM_SDK_SECRET
  if (!server || !secret) {
    throw new Error('请配置 Popo IM 地址和 SDKSecret')
  }

  const response = await fetch(`${server}/app/sdk/token`, {
    method: 'POST',
    redirect: 'error',
    signal: AbortSignal.timeout(10_000),
    headers: {
      Authorization: `Bearer ${secret}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(user)
  })
  if (!response.ok) throw new Error('IM 取 Token 请求失败')
  const result = await response.json()
  if (result?.code !== 0) throw new Error('IM 取 Token 业务失败')
  const data = result.data
  if (!data || data.user?.user_id !== user.user_id
    || typeof data.token !== 'string' || !data.token.trim()
    || !Number.isSafeInteger(data.expire_at)
    || data.expire_at <= Math.floor(Date.now() / 1000)) {
    throw new Error('IM 认证结果无效')
  }
  return data
}
csharp
// .NET 8+,仅使用内置库。
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading.Tasks;

public static class ImAuth
{
    private static readonly HttpClient Http = new(
        new HttpClientHandler { AllowAutoRedirect = false })
    {
        Timeout = TimeSpan.FromSeconds(10)
    };

    public static async Task<JsonElement> FetchImAuth(Dictionary<string, string> user)
    {
        var server = Environment.GetEnvironmentVariable("POPOIM_SERVER_URL")?.TrimEnd('/');
        var secret = Environment.GetEnvironmentVariable("POPOIM_SDK_SECRET");
        if (string.IsNullOrWhiteSpace(server) || string.IsNullOrWhiteSpace(secret))
            throw new InvalidOperationException("请配置 Popo IM 地址和 SDKSecret");

        using var request = new HttpRequestMessage(HttpMethod.Post, server + "/app/sdk/token");
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secret);
        request.Content = JsonContent.Create(user);
        using var response = await Http.SendAsync(request);
        if (!response.IsSuccessStatusCode)
            throw new HttpRequestException("IM 取 Token 请求失败");
        using var json = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
        var root = json.RootElement;
        if (!root.TryGetProperty("code", out var code)
            || !code.TryGetInt32(out var value) || value != 0)
            throw new InvalidOperationException("IM 取 Token 业务失败");

        var data = root.GetProperty("data");
        if (data.GetProperty("user").GetProperty("user_id").GetString() != user["user_id"]
            || string.IsNullOrWhiteSpace(data.GetProperty("token").GetString())
            || !data.GetProperty("expire_at").TryGetInt64(out var expiry)
            || expiry <= DateTimeOffset.UtcNow.ToUnixTimeSeconds())
            throw new InvalidOperationException("IM 认证结果无效");
        return data.Clone(); // JsonDocument 释放后,返回值仍可使用。
    }
}

五个函数都返回 Popo IM 响应中的 data 对象,包含 user、token、expire_at,以及服务端返回的 push。字段保持 HTTP 的命名;客户端 Provider 再转换成 userId、expireAt 等 SDK 模型字段。

接到你自己的认证接口 ​

下面是框架无关的处理顺序,对应客户端要请求的 POST /api/im-auth:

text
1. 用你现有的登录中间件校验 Session / Access Token。
   未登录或已失效 → 返回 HTTP 401,结束请求。
2. 从当前登录用户记录构造 user_id、昵称、账号、头像和个性签名。
3. 调用上面所选语言的取 Token 函数,得到 data。
4. 设置响应头 Cache-Control: no-store。
5. 返回 JSON:{ "code": 0, "msg": "ok", "data": data }。

如果使用 Cookie 登录,沿用你项目现有的 CSRF 防护。函数抛错或 Go 返回 error 时,由接口统一处理:上游异常可返回 HTTP 502 和你自己的业务错误码,不把原始上游响应、Token、请求头或密钥写入错误响应和日志。宿主登录失效使用 401,避免客户端把 IM 服务临时故障误判成用户退出。

这份最小实现每次向 Popo IM 获取有效 Token。先打通链路,再按实际需要加服务端缓存;缓存必须按用户隔离,在资料变更及到期前失效,不能把陈悦的凭证返回给林舟。

需要推送配置时,再从宿主请求中读取并校验 platform、manufacturer、package_name 三项,将它们一并提交给 Popo IM;用户身份仍由后端登录态确定。字段约束见取 Token 接口。

验证后再接客户端 ​

  1. 陈悦登录你的产品,调用 /api/im-auth,确认 code 为 0、data.user.user_id 为 chenyue,expire_at 大于当前 Unix 秒时间戳。
  2. 退出宿主登录后再请求,确认返回 401;传入其他人的 user_id 也不能替其取 Token。
  3. 用林舟的宿主账号请求,确认返回的是林舟的资料和凭证。

本地调试时只检查必要字段,不将完整 Token 粘贴到公共 JWT 解析网站。下一步选择 Web、Android、iOS、HarmonyOS 或 uni-app,将此接口配置到客户端 authProvider。