跳转到内容

认证与授权

认证与授权是 RTC Agent 的安全基石。RTC Agent 支持多种认证方式,其中 Token Exchange(RFC 8693) 是推荐给企业集成场景的首选方案——它允许你将 RTC Agent 无缝集成到已有的认证系统中,无需实现完整的 OAuth2 流程。

Token Exchange 是 OAuth2 扩展标准(RFC 8693),专为跨系统信任设计。相比传统 OAuth2 授权码模式,它更适合企业集成场景:

特性Token ExchangeOAuth2 授权码模式
适用场景已有认证系统的企业应用独立应用,需要完整登录流程
集成复杂度✅ 低——只需签发可验证的 JWT❌ 高——需实现授权页面、回调处理等
用户体验✅ 无缝——用户无需重复登录⚠️ 需要跳转到登录页
安全性✅ 高——基于 JWKS 的非对称签名✅ 高——基于授权码 + PKCE
多租户支持✅ 天然支持——JWT 包含用户信息⚠️ 需要额外处理

核心优势:

  • 🚀 快速集成:3 分钟即可完成集成
  • 🔒 零密码接触:RTC Agent 不接触用户密码,安全由你的系统保障
  • 🌐 跨域友好:无需处理复杂的 OAuth2 回调和重定向
  • 📱 多端同步:JWT 天然支持多设备、多标签页

💡 设计原则:Token Exchange 模式下,你的系统负责签发 JWT,RTC Agent 负责验证并换取内部访问令牌。整个过程无需用户交互,完全自动化。

本指南将带你完成最简单的 Token Exchange 集成。假设你已经有:

  • 一个能够签发 JWT 的后端服务
  • 一个使用 RTC Agent 的前端应用

步骤 1:配置主服务器信任你的 JWT 签发方

Section titled “步骤 1:配置主服务器信任你的 JWT 签发方”

在 RTC Agent Server 的 config.yaml 中添加:

token_exchange:
external_issuers:
- name: "your-app" # 人类可读的标识
issuer: "https://your-app.com" # 你的 JWT 签发方标识(必须与 JWT 中的 iss 一致)
jwks_uri: "https://your-app.com/.well-known/jwks.json" # JWKS 端点
allowed_algorithms: ["RS256"] # 允许的签名算法
cache_ttl: 3600 # JWKS 缓存时间(秒)
# claims_mapping 仅在 JWT 使用非标准 claim 名称时才需要配置
# 默认映射已覆盖常见名称:email→email, name→name, picture→avatar_url
# sub 始终从 JWT 的 "sub" claim 读取,不可通过 claims_mapping 更改

在你的前端应用中配置 RTC Agent:

import { createRtcAgent } from '@rtc-agent/component';
const agent = createRtcAgent({
server: { url: 'https://rtc-agent.your-app.com' },
auth: {
type: 'token-exchange',
// 返回你的系统签发的 JWT
getExchangeToken: async () => {
return await yourAuthStore.getAccessToken();
},
// 检查用户是否已登录
isLoggedIn: () => {
return yourAuthStore.isAuthenticated();
},
// 登出处理
logout: async () => {
await yourAuthStore.clearSession();
},
// 返回当前用户唯一标识(用于 IndexedDB 隔离)
getUserId: () => {
return yourAuthStore.getUserId();
},
// 设备唯一标识(UUID,持久化到 localStorage)
deviceId: crypto.randomUUID(),
},
});
document.body.appendChild(agent);

完成配置后,打开浏览器控制台检查:

  1. ✅ 没有 CORS 错误
  2. ✅ 没有 JWT 验证失败的错误
  3. ✅ WebSocket 连接成功建立
  4. ✅ 能够正常发送消息和调用工具

🎉 恭喜! 如果以上步骤都成功,你的 RTC Agent 已经通过 Token Exchange 集成到你的认证系统中了。


本节提供 Token Exchange 的完整技术细节,帮助你构建生产级集成。

流程说明:

  1. 用户登录宿主应用
  2. 宿主应用后端签发 JWT(包含用户信息)
  3. 前端通过 getExchangeToken() 提供 JWT
  4. RTC Agent 组件调用 Token Exchange 端点
  5. RTC Agent Server 从 JWKS 端点获取公钥
  6. 验证 JWT 签名和 claims
  7. 签发 RTC Agent 内部 JWT
  8. 使用 RTC JWT 建立 WebSocket 连接

你的后端服务需要实现 JWT 签发逻辑。以下是关键要求:

必须包含的 Claims:

Claim类型说明示例
issstringJWT 签发方标识,必须与 config.yaml 中的 issuer 一致"https://your-app.com"
substring用户唯一标识(在你的系统中)"user-123"
expnumber过期时间(Unix 时间戳,秒)1699999999
iatnumber签发时间(Unix 时间戳,秒)1699996399

推荐包含的 Claims:

Claim类型说明示例
emailstring用户邮箱"user@example.com"
namestring用户显示名称"张三"
picturestring用户头像 URL"https://example.com/avatar.png"
audstring[]受众(RTC Agent Server 标识)["https://rtc-agent.your-app.com"]
jtistringJWT ID(唯一标识,防止重放攻击)"uuid-string"

RTC Agent 支持以下非对称签名算法:

算法说明推荐度
RS256RSA + SHA-256⭐⭐⭐⭐⭐ 默认推荐
RS384RSA + SHA-384⭐⭐⭐⭐
RS512RSA + SHA-512⭐⭐⭐⭐
ES256ECDSA + SHA-256⭐⭐⭐⭐⭐ 性能更好
ES384ECDSA + SHA-384⭐⭐⭐⭐
ES512ECDSA + SHA-512⭐⭐⭐⭐
EdDSAEd25519⭐⭐⭐⭐⭐ 最新标准

💡 推荐:生产环境使用 RS256 或 ES256。ES256 性能更好,但 RS256 兼容性更广。

生成密钥对(以 RS256 为例):

终端窗口
# 生成 RSA 私钥(2048 位)
openssl genpkey -algorithm RSA -out private.pem -pkeyopt rsa_keygen_bits:2048
# 从私钥提取公钥
openssl rsa -pubout -in private.pem -out public.pem

存储密钥:

  • 私钥:仅后端服务可访问,权限 0600
  • 公钥:可通过 JWKS 端点公开访问,权限 0644

Go 语言示例(使用 golang-jwt):

package main
import (
"crypto/rsa"
"os"
"time"
"github.com/golang-jwt/jwt/v5"
)
// 加载私钥
func loadPrivateKey(path string) (*rsa.PrivateKey, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
return jwt.ParseRSAPrivateKeyFromPEM(data)
}
// 签发 JWT
func signJWT(userID, email, name string, privateKey *rsa.PrivateKey) (string, error) {
now := time.Now()
claims := jwt.MapClaims{
"iss": "https://your-app.com", // 签发方
"sub": userID, // 用户 ID
"email": email, // 邮箱
"name": name, // 显示名称
"iat": now.Unix(), // 签发时间
"exp": now.Add(1 * time.Hour).Unix(), // 过期时间(1 小时)
"jti": uuid.NewString(), // JWT ID
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
token.Header["kid"] = "your-key-id" // 密钥 ID
return token.SignedString(privateKey)
}

Node.js 示例(使用 jose):

import { SignJWT, importPKCS8 } from 'jose';
import { readFileSync } from 'fs';
// 加载私钥
const privateKeyPem = readFileSync('./private.pem', 'utf8');
const privateKey = await importPKCS8(privateKeyPem, 'RS256');
// 签发 JWT
async function signJWT(userID, email, name) {
const now = Math.floor(Date.now() / 1000);
const token = await new SignJWT({
sub: userID,
email: email,
name: name,
})
.setProtectedHeader({ alg: 'RS256', kid: 'your-key-id' })
.setIssuedAt(now)
.setExpirationTime(now + 3600) // 1 小时
.setIssuer('https://your-app.com')
.sign(privateKey);
return token;
}

Python 示例(使用 PyJWT):

import jwt
import uuid
from datetime import datetime, timedelta
from cryptography.hazmat.primitives import serialization
# 加载私钥
with open('./private.pem', 'rb') as f:
private_key = serialization.load_pem_private_key(f.read(), password=None)
# 签发 JWT
def sign_jwt(user_id: str, email: str, name: str) -> str:
now = datetime.utcnow()
claims = {
'iss': 'https://your-app.com',
'sub': user_id,
'email': email,
'name': name,
'iat': now,
'exp': now + timedelta(hours=1),
'jti': str(uuid.uuid4()),
}
headers = {
'kid': 'your-key-id',
'alg': 'RS256',
}
return jwt.encode(claims, private_key, algorithm='RS256', headers=headers)

JWKS(JSON Web Key Set)端点允许 RTC Agent Server 获取公钥来验证 JWT 签名。

{
"keys": [
{
"kty": "RSA",
"kid": "your-key-id",
"use": "sig",
"alg": "RS256",
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx...",
"e": "AQAB"
}
]
}

字段说明:

字段说明
kty密钥类型(RSA / EC / OKP)
kid密钥 ID(与 JWT header 中的 kid 一致)
use密钥用途(sig = 签名)
alg算法(RS256 / ES256 等)
n, eRSA 公钥参数(模数和指数)

Go 语言(使用 go-jose):

package main
import (
"crypto/rsa"
"encoding/json"
"net/http"
"github.com/go-jose/go-jose/v3"
)
func jwksHandler(publicKey *rsa.PublicKey, keyID string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
jwk := jose.JSONWebKey{
Key: publicKey,
KeyID: keyID,
Algorithm: "RS256",
Use: "sig",
}
jwks := jose.JSONWebKeySet{
Keys: []jose.JSONWebKey{jwk},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(jwks)
}
}
// 注册路由
http.HandleFunc("/.well-known/jwks.json", jwksHandler(publicKey, "your-key-id"))

Node.js(使用 jose):

import { exportJWK } from 'jose';
import express from 'express';
const app = express();
app.get('/.well-known/jwks.json', async (req, res) => {
const jwk = await exportJWK(publicKey);
jwk.kid = 'your-key-id';
jwk.alg = 'RS256';
jwk.use = 'sig';
res.json({
keys: [jwk],
});
});

Python(使用 jwcrypto):

from flask import Flask, jsonify
from jwcrypto import jwk
import json
app = Flask(__name__)
@app.route('/.well-known/jwks.json')
def jwks():
with open('./public.pem', 'rb') as f:
key = jwk.JWK.from_pem(f.read())
key['kid'] = 'your-key-id'
key['alg'] = 'RS256'
key['use'] = 'sig'
return jsonify(keys=[json.loads(key.export_public())])

JWKS 端点应该设置合适的缓存头:

Cache-Control: public, max-age=3600

RTC Agent Server 会在内部缓存 JWKS(默认 1 小时),减少对你的服务器的请求。

前端通过 createRtcAgent() 的 auth 字段配置 Token Exchange 模式。

interface AuthProvider {
// 认证模式:固定为 'token-exchange'
type: 'token-exchange';
// 返回外部 JWT(你的系统签发的 JWT)(可同步或异步)
getExchangeToken(): string | Promise<string>;
// 检查用户是否已登录(同步)
isLoggedIn(): boolean;
// 登出处理(异步)
logout?(): Promise<void>;
// 返回当前用户唯一标识(同步,强烈建议提供)
getUserId?(): string;
// 设备唯一标识(必填)
deviceId: string;
}
import { createRtcAgent } from '@rtc-agent/component';
// 假设你的认证存储
class YourAuthStore {
getAccessToken(): string | null {
return localStorage.getItem('access_token');
}
isAuthenticated(): boolean {
const token = this.getAccessToken();
if (!token) return false;
// 检查是否过期
const expiry = localStorage.getItem('token_expiry');
if (!expiry) return false;
return Date.now() < parseInt(expiry);
}
getUserId(): string {
const userInfo = JSON.parse(localStorage.getItem('user_info') || '{}');
return userInfo.id || '';
}
async clearSession(): Promise<void> {
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
localStorage.removeItem('user_info');
localStorage.removeItem('token_expiry');
}
}
const authStore = new YourAuthStore();
// 生成或获取 Device ID(持久化)
function getDeviceId(): string {
let deviceId = localStorage.getItem('device_id');
if (!deviceId) {
deviceId = crypto.randomUUID();
localStorage.setItem('device_id', deviceId);
}
return deviceId;
}
// 创建 RTC Agent
const agent = createRtcAgent({
server: { url: 'https://rtc-agent.your-app.com' },
auth: {
type: 'token-exchange',
getExchangeToken: async () => {
const token = authStore.getAccessToken();
if (!token) {
throw new Error('No access token available');
}
return token;
},
isLoggedIn: () => authStore.isAuthenticated(),
logout: async () => {
await authStore.clearSession();
},
getUserId: () => authStore.getUserId(),
deviceId: getDeviceId(),
},
});
// 添加到 DOM
document.body.appendChild(agent);

RTC Agent 组件会在以下时机调用 getExchangeToken():

  1. 初次连接:建立 WebSocket 时
  2. 令牌即将过期:到期前 5 分钟自动刷新
  3. 页面切回前台:从后台切回时检查
  4. WebSocket 断开重连:重新建立连接时

建议在 getExchangeToken() 中实现自动刷新逻辑:

getExchangeToken: async () => {
// 检查是否即将过期(提前 5 分钟)
if (isTokenExpiringSoon()) {
// 刷新 token
const newToken = await refreshToken();
saveToken(newToken);
return newToken;
}
return getAccessToken();
}

在主服务器的 config.yaml 中配置信任的外部 JWT 签发方:

token_exchange:
external_issuers:
- name: "your-app" # 人类可读的标识
issuer: "https://your-app.com" # JWT iss claim 的期望值
jwks_uri: "https://your-app.com/.well-known/jwks.json"
allowed_algorithms: ["RS256", "ES256"] # 允许的签名算法
cache_ttl: 3600 # JWKS 缓存时间(秒)
# claims_mapping 仅在 JWT 使用非标准 claim 名称时才需要配置
# 默认映射已覆盖常见名称:email→email, name→name, picture→avatar_url

配置说明:

字段必填说明
name✅人类可读的标识符
issuer✅JWT iss claim 的期望值(必须与 JWT 中的 iss 一致)
jwks_uri✅JWKS 端点 URL
allowed_algorithms✅允许的签名算法列表
cache_ttl❌JWKS 缓存时间(秒),默认 3600
claims_mapping❌JWT claims 映射关系

多个签发方:可以配置多个外部签发方,支持多租户场景:

token_exchange:
external_issuers:
- name: "admin-server"
issuer: "http://admin-server:8081"
jwks_uri: "http://admin-server:8081/.well-known/jwks.json"
# ...
- name: "customer-a"
issuer: "https://customer-a.com"
jwks_uri: "https://customer-a.com/.well-known/jwks.json"
# ...
- name: "customer-b"
issuer: "https://customer-b.com"
jwks_uri: "https://customer-b.com/.well-known/jwks.json"
# ...
Claim类型说明验证规则
issstring签发方标识必须与 config.yaml 中的 issuer 一致
substring用户唯一标识在你的系统中必须唯一且稳定
expnumber过期时间(Unix 时间戳)必须大于当前时间
iatnumber签发时间(Unix 时间戳)必须小于等于当前时间
Claim类型说明映射配置
emailstring用户邮箱claims_mapping.email: "email"
namestring用户显示名称claims_mapping.name: "name"
picturestring用户头像 URLclaims_mapping.avatar_url: "picture"
audstring[]受众可选,用于限制 JWT 的使用范围
jtistringJWT ID可选,用于防止重放攻击

claims_mapping 允许你将 JWT 中的自定义 claim 名称映射到 RTC Agent 的标准字段。配置格式为 标准字段: JWT 中的 claim 名称。

默认映射(无需配置即可工作):

标准字段默认读取的 JWT claim说明
emailemail用户邮箱
namename用户显示名称
avatar_urlpicture用户头像

注意:sub(用户唯一标识)始终从 JWT 的 sub claim 读取,不可通过 claims_mapping 更改。

仅当你的 JWT 使用非标准 claim 名称时才需要配置 claims_mapping。例如,JWT 中邮箱字段为 email_address,显示名称为 display_name:

claims_mapping:
email: "email_address" # 从 JWT 的 email_address claim 读取邮箱
name: "display_name" # 从 JWT 的 display_name claim 读取显示名称
avatar_url: "avatar" # 从 JWT 的 avatar claim 读取头像 URL

  • ✅ 私钥安全:私钥仅后端服务可访问,权限设置为 0600
  • ✅ 定期轮换:定期更换密钥对(建议每年)
  • ✅ 密钥备份:妥善备份私钥,防止丢失
  • ❌ 禁止共享:永远不要共享私钥或通过不安全渠道传输
  • ✅ 使用非对称算法:RS256 / ES256 / EdDSA,不要使用 HS256(对称算法)
  • ✅ 设置合理的过期时间:建议 1 小时,最长不超过 24 小时
  • ✅ 包含 jti claim:防止 JWT 重放攻击
  • ✅ 验证 audience:如果 JWT 有 aud claim,确保它包含 RTC Agent Server
  • ✅ 使用 HTTPS:生产环境必须使用 HTTPS(localhost 除外)
  • ✅ 设置缓存头:Cache-Control: public, max-age=3600
  • ✅ 限制响应大小:防止恶意请求导致资源耗尽
  • ✅ 监控访问日志:检测异常请求模式
  • ✅ Token 存储:使用 localStorage 存储 JWT,设置适当的过期时间
  • ✅ XSS 防护:确保应用没有 XSS 漏洞
  • ✅ CSRF 防护:对于敏感操作,使用 CSRF token
  • ✅ 自动刷新:实现 token 自动刷新,避免用户频繁重新登录

错误信息:token_exchange.exchange_rejected: invalid_signature

可能原因:

  • ❌ JWT 签名算法与配置不匹配
  • ❌ 公钥未正确部署到 JWKS 端点
  • ❌ JWT 被篡改

解决方案:

  1. 检查 config.yaml 中的 allowed_algorithms 是否包含 JWT 使用的算法
  2. 访问 JWKS 端点,确认返回的公钥正确
  3. 使用 JWT 调试工具(如 jwt.io)验证签名

错误信息:token_exchange.exchange_rejected: invalid_issuer

可能原因:

  • ❌ JWT 的 iss claim 与 config.yaml 中的 issuer 不一致

解决方案:

  • 确保 JWT 的 iss 与配置完全一致(包括协议和端口)

错误信息:token_exchange.internal_error: failed to fetch JWKS

可能原因:

  • ❌ JWKS 端点不可访问
  • ❌ JWKS 端点返回非 JSON 响应
  • ❌ 网络问题

解决方案:

  1. 检查 JWKS 端点是否可访问
  2. 确认响应格式正确(Content-Type: application/json)
  3. 检查 RTC Agent Server 的日志获取详细错误信息

症状:RTC 脚本执行被过滤,无法正常运行

可能原因:

  • ❌ 前端传递的 deviceId 与 JWT 中的不一致

解决方案:

  • 确保 AuthProvider.deviceId 持久化到 localStorage,刷新页面后保持一致
  • 参考 admin-ui 的实现:getOrCreateDeviceId()

RTC Agent 自带的 Admin UI 是一个完整的 Token Exchange 集成示例。它展示了如何将 RTC Agent 集成到已有的认证系统中。

Server 端配置(admin.yaml):

jwt:
algorithm: "RS256"
issuer: "http://admin-server:8081"
audience: "http://server-1:8888"
private_key_path: "/app/etc/keys/admin-private.pem"
public_key_path: "/app/etc/keys/admin-public.pem"
access_token_ttl: 3600 # 1 小时
refresh_token_ttl: 604800 # 7 天

Client 端配置(rtc-auth-provider.ts):

export function createAdminAuthProvider(): AuthProvider {
return {
type: 'token-exchange',
getExchangeToken: async (): Promise<string> => {
// 检查是否需要刷新(提前 5 分钟)
if (isTokenExpiringSoon()) {
const refreshToken = getRefreshToken();
if (refreshToken) {
// 刷新 token
const result = await apiRefreshToken({ refresh_token: refreshToken });
setTokens(result.access_token, result.refresh_token, result.expires_in || 3600);
return result.access_token;
}
}
const token = getAccessToken();
if (!token) {
throw new Error('No admin access token available');
}
return token;
},
isLoggedIn: (): boolean => {
return isAuthenticated();
},
logout: async (): Promise<void> => {
const refreshToken = getRefreshToken();
if (refreshToken) {
try {
await apiLogout({ refresh_token: refreshToken });
} catch (error) {
console.warn('Failed to revoke refresh token:', error);
}
}
clearAuth();
},
getUserId: (): string => {
const userInfo = getUserInfo();
return userInfo?.id ?? '';
},
deviceId: getOrCreateDeviceId(),
};
}

完整代码请参考:



对于独立应用或需要完整 OAuth2 流程的场景,RTC Agent 也支持传统的 OAuth2 授权码模式。详见 OAuth2 集成指南。

💡 提示:如果你的应用已经有自己的认证系统,强烈推荐使用 Token Exchange 而非 OAuth2。Token Exchange 集成更简单、用户体验更好、且更适合企业场景。