外观
后端认证示例
这一页实现认证与登录中的后端步骤:你的后端用 SDKSecret 调用 Popo IM,为已经登录你产品的当前用户获取 IM Token。选择你的后端语言即可,客户端 SDK 的接入语言不影响这里的选择。
这里的认证流程是携带密钥换 Token,无需自行计算签名。 JWT 的 HS256 签发与验签由 Popo IM 完成,算法说明见认证和签名算法。
先准备这两项配置
在业务后端的运行环境中设置:
| 环境变量 | 内容 |
|---|---|
POPOIM_SERVER_URL | Popo IM 部署根地址,例如 https://im.example.com |
POPOIM_SDK_SECRET | Popo 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 接口。
验证后再接客户端
- 陈悦登录你的产品,调用
/api/im-auth,确认code为0、data.user.user_id为chenyue,expire_at大于当前 Unix 秒时间戳。 - 退出宿主登录后再请求,确认返回 401;传入其他人的
user_id也不能替其取 Token。 - 用林舟的宿主账号请求,确认返回的是林舟的资料和凭证。
本地调试时只检查必要字段,不将完整 Token 粘贴到公共 JWT 解析网站。下一步选择 Web、Android、iOS、HarmonyOS 或 uni-app,将此接口配置到客户端 authProvider。
