OAuth2 集成指南
OAuth2 允许第三方应用在用户授权后访问其 MindVerse 数据
OAuth2 允许第三方应用在用户授权后访问其 MindVerse 数据。本指南介绍如何实现标准的授权码流程。
概述
SecondMe API 使用标准的 OAuth2 授权码流程 (Authorization Code Flow):
- 用户被重定向到 MindVerse 授权页面
- 用户确认授权
- MindVerse 返回授权码到你的应用
- 你的应用用授权码换取 Access Token
- 获取并保存
appScopedUserId - 使用 Access Token 调用 API
- 在用户撤销授权时接收 webhook 通知
授权流程时序图
下图展示了完整的 OAuth2 授权码流程:
前提条件
在开始之前,你需要:
- 在 SecondMe Developer Console 注册应用
- 获取
client_id和client_secret - 配置回调 URL (Redirect URI)
令牌类型和有效期
| 令牌类型 | 前缀 | 有效期 |
|---|---|---|
| Authorization Code | lba_ac_ | 5 分钟 |
| Access Token | lba_at_ | 7 天 |
| Refresh Token | lba_rt_ | 365 天 |
授权流程
步骤 1: 发起授权请求
引导用户访问 SecondMe 授权页面进行登录和授权。
将用户重定向到 SecondMe 授权页面:
https://go.second-me.cn/oauth/?client_id=YOUR_CLIENT_ID&redirect_uri=YOUR_REDIRECT_URI&response_type=code&state=RANDOM_STATE授权 URL 参数:
| 参数 | 类型 | 必需 | 说明 |
|---|---|---|---|
| client_id | string | 是 | 应用的 Client ID |
| redirect_uri | string | 是 | 授权后的回调 URL,必须与应用配置一致 |
| response_type | string | 是 | 固定值 code |
| state | string | 是 | CSRF 保护参数,建议使用随机字符串 |
前端示例代码:
// 构建授权 URL
function buildAuthorizationUrl() {
const params = new URLSearchParams({
client_id: 'your_client_id',
redirect_uri: 'https://your-app.com/callback',
response_type: 'code',
state: generateRandomState() // 生成随机 state 并存储用于验证
});
return `https://go.second-me.cn/oauth/?${params.toString()}`;
}
// 发起授权 - 方式1: 直接跳转
window.location.href = buildAuthorizationUrl();
// 发起授权 - 方式2: 新窗口打开
window.open(buildAuthorizationUrl(), '_blank');用户在 SecondMe 完成登录和授权后,会被重定向回你的 redirect_uri,URL 中包含授权码:
https://your-app.com/callback?code=lba_ac_xxxxx...&state=your_state重要: 收到回调后,务必验证
state参数与发起请求时存储的值一致,以防止 CSRF 攻击。
步骤 2: 用授权码换取 Token
收到授权码后,在服务端用授权码换取 Access Token:
curl -X POST "https://api.mindverse.com/gate/lab/api/oauth/token/code" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=lba_ac_xxxxx..." \
-d "redirect_uri=https://your-app.com/callback" \
-d "client_id=your_client_id" \
-d "client_secret=your_client_secret"注意: 必须使用
application/x-www-form-urlencoded格式发送请求体,不能使用 JSON 格式(application/json)。使用错误的格式会导致Field required验证错误。
请求参数:
| 参数 | 类型 | 必需 | 说明 |
|---|---|---|---|
| grant_type | string | 是 | 固定值 authorization_code |
| code | string | 是 | 步骤 1 获取的授权码 |
| redirect_uri | string | 是 | 必须与步骤 1 中的值一致 |
| client_id | string | 是 | 应用的 Client ID |
| client_secret | string | 是 | 应用的 Client Secret |
成功响应:
{
"code": 0,
"data": {
"accessToken": "lba_at_xxxxx...",
"refreshToken": "lba_rt_xxxxx...",
"tokenType": "Bearer",
"expiresIn": 7200,
"scope": ["userinfo", "chat.read", "chat.write"]
}
}步骤 3: 获取并保存 appScopedUserId
在 Access Token 换取成功后,建议立即调用 GET /api/auth/me 获取当前授权关系对应的 appScopedUserId,并保存到你自己的用户绑定关系中。
当用户后续在 SecondMe 侧主动取消对你应用的授权时,平台发送的 authorization.revoked webhook 将只携带 appScopedUserId 作为授权关系标识。你的服务端需要依靠这个字段定位本地用户,并撤销本地登录态、账号绑定或访问权限。
curl -X GET "https://api.mindverse.com/gate/lab/api/auth/me" \
-H "Authorization: Bearer lba_at_xxxxx..."成功响应:
{
"code": 0,
"data": {
"userId": "u_xxxxx...",
"name": "张三",
"email": "zhangsan@example.com",
"avatar": "https://example.com/avatar.png",
"bio": "个人简介",
"appScopedUserId": "asu_xxxxx..."
}
}字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
| userId | string | 平台内部用户 ID |
| appScopedUserId | string | 当前用户在你的应用维度下的稳定授权标识 |
重要:
appScopedUserId在同一个应用内稳定,但不同应用之间不可比较。它不能替代平台内部userId,也不应该暴露给其他应用作为通用用户标识。
步骤 4: 使用 Access Token
在 API 请求中使用 Access Token:
curl -X GET "https://api.mindverse.com/gate/lab/api/secondme/user/info" \
-H "Authorization: Bearer lba_at_xxxxx..."步骤 5: 处理授权撤销 webhook
当用户在 SecondMe 中主动取消对你的应用授权后,平台会向你在应用配置中填写的 webhook 地址发送 authorization.revoked 事件。
配置 webhook
在应用配置页中,你需要配置:
- Authorization revoked webhook URL
- Webhook secret
Webhook secret 只会在创建或更新时明文返回一次。平台只保存加密后的版本,你的服务端需要安全保存这份 secret,用于后续验签。
如果控制台提供“发送测试”能力,你可以用它立即验证当前填写的 URL、secret、验签和接收逻辑是否正确,而不会影响真实授权关系。
事件定义
Headers:
| Header | 说明 |
|---|---|
Content-Type | 固定为 application/json |
X-SecondMe-Event-Id | 事件唯一 ID |
X-SecondMe-Timestamp | Unix 秒级时间戳 |
X-SecondMe-Signature | HMAC-SHA256 十六进制小写签名 |
Payload:
{
"eventId": "evt_xxx",
"eventType": "authorization.revoked",
"occurredAt": "2026-04-13T14:30:00Z",
"appId": "app_xxx",
"appScopedUserId": "asu_xxx",
"reason": "user_revoked"
}| 字段 | 类型 | 说明 |
|---|---|---|
eventId | string | 事件唯一 ID,可用于幂等去重 |
eventType | string | 固定为 authorization.revoked |
occurredAt | string | 事件发生时间,ISO 8601 格式 |
appId | string | 触发事件的应用 ID |
appScopedUserId | string | 用于定位你本地用户绑定关系的授权标识 |
reason | string | 第一版固定为 user_revoked |
测试投递时,eventType 仍然是 authorization.revoked,reason 会使用 test_delivery。
签名规则
平台使用你的 webhook secret 对以下字符串做 HMAC-SHA256:
{timestamp}.{raw_body}其中:
timestamp对应X-SecondMe-Timestampraw_body必须是 HTTP 请求的原始 body 字符串,不能先反序列化再重新序列化- 签名结果以十六进制小写字符串形式放在
X-SecondMe-Signature中
const crypto = require('crypto');
function verifySecondMeSignature({ timestamp, rawBody, signature, secret }) {
const payload = `${timestamp}.${rawBody}`;
const expected = crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature, 'utf8'),
Buffer.from(expected, 'utf8')
);
}服务端处理建议
收到 webhook 后,建议按以下顺序处理:
- 校验
X-SecondMe-Signature - 校验
X-SecondMe-Timestamp是否在 5 分钟容忍窗口内 - 使用
eventId做幂等去重 - 根据
appScopedUserId定位本地用户绑定关系 - 清理本地 session、登录态、账号绑定或访问权限
- 返回
2xx
时间戳校验示例:
const nowSeconds = Math.floor(Date.now() / 1000);
const webhookTimestamp = Number(timestamp);
if (Math.abs(nowSeconds - webhookTimestamp) > 300) {
throw new Error('Webhook timestamp expired');
}重试规则
平台会在以下情况下自动重试 webhook 投递:
- 网络超时
- 连接失败
- 响应为
408 - 响应为
429 - 响应为
5xx
普通 4xx 默认不会重试。
步骤 6: 刷新 Access Token
当 Access Token 过期时,使用 Refresh Token 获取新的 Access Token:
curl -X POST "https://api.mindverse.com/gate/lab/api/oauth/token/refresh" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token" \
-d "refresh_token=lba_rt_xxxxx..." \
-d "client_id=your_client_id" \
-d "client_secret=your_client_secret"注意: 必须使用
application/x-www-form-urlencoded格式发送请求体,不能使用 JSON 格式(application/json)。使用错误的格式会导致Field required验证错误。
请求参数:
| 参数 | 类型 | 必需 | 说明 |
|---|---|---|---|
| grant_type | string | 是 | 固定值 refresh_token |
| refresh_token | string | 是 | 之前获取的 Refresh Token |
| client_id | string | 是 | 应用的 Client ID |
| client_secret | string | 是 | 应用的 Client Secret |
成功响应:
{
"code": 0,
"data": {
"accessToken": "lba_at_new_token...",
"refreshToken": "lba_rt_new_token...",
"tokenType": "Bearer",
"expiresIn": 7200,
"scope": ["userinfo", "chat.read", "chat.write"]
}
}注意: 刷新 Token 接口不再轮换 Refresh Token。响应中的
refreshToken与请求中传入的值相同,可在有效期内继续复用。
权限 (Scope)
请求授权时需要指定权限列表。用户可以看到你的应用请求的权限,并决定是否授权。
| 权限 | 说明 |
|---|---|
userinfo | 访问用户信息(姓名、邮箱、头像、简介、兴趣标签) |
memory.read | 搜索 Key Memory |
chat.read | 查看聊天会话列表和消息历史 |
chat.write | 发送消息和流式聊天 |
note.write | 添加笔记和记忆 |
voice | 使用语音合成功能 |
plaza.read | 浏览广场动态、帖子详情和评论 |
plaza.write | 发帖和评论 |
agent_memory | 上报和查询 Agent Memory 事件 |
最佳实践: 只请求必要的权限,避免请求过多权限导致用户拒绝授权。
错误处理
授权码无效或过期
{
"code": 400,
"message": "授权码无效或已过期",
"subCode": "oauth2.code.invalid"
}Client Secret 错误
{
"code": 401,
"message": "Client Secret 不匹配",
"subCode": "oauth2.client.secret_mismatch"
}Redirect URI 不匹配
{
"code": 400,
"message": "Redirect URI 不匹配",
"subCode": "oauth2.redirect_uri.mismatch"
}Access Token 过期
{
"code": 401,
"message": "Access Token 已过期",
"subCode": "oauth2.token.expired"
}安全最佳实践
1. 保护 Client Secret
Client Secret 必须保密,只能在服务端使用,不要在客户端代码中暴露。
2. 验证 Redirect URI
确保 Redirect URI 使用 HTTPS,并且已在SecondMe后台注册。
本地开发: 本地地址的 Redirect URI(
http://localhost:*和http://127.0.0.1:*)在 OAuth2 授权流程中会自动放行,无需在开发者后台预先注册。这样在本地开发调试时可以随意更换端口,无需每次更新应用配置。
3. 安全存储 Token
- 服务端存储时加密 Token
- 不要在日志中记录 Token
- 设置合适的过期时间
4. 安全存储 Webhook Secret
- 仅在服务端读取和使用 webhook secret
- 不要把 webhook secret 暴露到前端环境变量
- 验签时始终使用原始请求体,不要使用重组后的 JSON 字符串
在线调试
我们提供了一个交互式工具,可以直接在浏览器中测试完整的 OAuth2 授权码流程。
下一步
- OAuth2 API 参考 - 查看完整 API 规格
- SecondMe API 参考 - 了解可用的 API 接口
- 错误码参考 - 了解所有错误码