OpenID Connect 协议文档
StarEcho 实现了标准的 OpenID Connect 1.0 协议(基于 OAuth 2.0 授权码模式),允许第三方应用接入节律星辰账号系统,实现「使用 StarEcho 登录」功能。
概览
| 项目 | 说明 |
|---|---|
| 协议 | OpenID Connect 1.0(Authorization Code Flow) |
| 签名算法 | RS256 |
| 支持的 Scope | openid、profile、email |
| 支持的授权类型 | authorization_code、refresh_token |
| 客户端认证 | client_secret_post、client_secret_basic、PKCE |
| Discovery 端点 | https://api.starecho.net/.well-known/openid-configuration |
授权码流程时序图
接入流程
1. 创建应用
登录 StarEcho,进入 个人中心 → 开发者 → 开发者应用,点击「创建应用」并填写:
| 字段 | 说明 |
|---|---|
| 应用名称 | 用户在授权页面看到的名称 |
| 应用描述 | 简要说明应用功能 |
| 账号用途说明 | 说明你的应用将如何使用 StarEcho 账号 |
| 回调地址 | OAuth 授权完成后的重定向 URI,须为 https:// |
| Web Origin(CORS) | 允许从浏览器调用 StarEcho API 的精确 Origin,例如 https://app.example.com,不包含路径 |
| 移动端 URL Scheme | 可选,用于移动端原生应用回调,如 starecho,完整回调地址为 starecho://callback |
| 应用主页 | 可选,应用官网地址 |
| 应用图标 | 可选,支持 JPG/PNG,建议 256×256 |
桌面端本地回调:
http://127.0.0.1:{port}/callback和http://localhost:{port}/callback(端口 1024-65535)自动允许,无需配置。这符合 RFC 8252 规范,本地回环地址的流量不会离开本机。
提交后需等待管理员审核。审核通过后才可使用。
应用级 Web Origin
Web Origin 与 OAuth 回调地址用途不同:回调地址决定授权完成后可以跳转到哪里;Web Origin 决定哪些网页可以通过浏览器跨域调用 StarEcho API。应用所有者可在创建应用或应用信息页维护 webOrigins,无需平台管理员修改服务器环境变量。
- 生产 Origin 必须使用 HTTPS,并且只能填写
协议 + 主机 + 端口,不能包含路径、查询参数、 Fragment、用户信息或通配符。 http://localhost:{port}与http://127.0.0.1:{port}仅用于开发,必须明确填写 1024–65535 端口。- 每个应用最多登记 32 个 Origin。空列表表示不允许任何外部网页调用应用的浏览器 API。
/cf/api/public/{clientId}/...与/cf/http/public/{clientId}/...会同时核对 URL 中的clientId和该应用的 Web Origin;其他需要授权的浏览器 API 仍会继续校验 Access Token、 Client ID 和权限。CORS 本身不是身份认证。STARECHO_CORS_ORIGIN仅用于平台自身的固定站点或运维 Origin,不应逐项登记开发者网站。
创建应用 POST /oauth/apps 与更新应用 PUT /oauth/apps/{clientId} 均接受:
{
"webOrigins": [
"https://app.example.com",
"http://localhost:5100"
]
}部署该能力时,必须先执行独立迁移 database_migrations/20260731-add-app-web-origins.sql,再部署新版平台包。迁移不会自动信任 任何现有网站;应用所有者需要显式保存 Web Origin。
2. 发起授权
将用户重定向到授权页面:
GET https://starecho.net/oauth/authorize查询参数:
| 参数 | 必填 | 说明 |
|---|---|---|
response_type | ✅ | 固定为 code |
client_id | ✅ | 你的应用 Client ID |
redirect_uri | ✅ | 回调地址,必须与注册时填写的一致 |
scope | ✅ | 请求的权限范围,空格分隔。例:openid profile email |
state | 推荐 | 随机字符串,用于防止 CSRF 攻击 |
nonce | 可选 | 随机字符串,将包含在 id_token 中,用于防止重放攻击 |
code_challenge | 可选 | PKCE 挑战码(使用 PKCE 时必填) |
code_challenge_method | 可选 | PKCE 方法:S256 或 plain |
示例:
https://starecho.net/oauth/authorize?
response_type=code&
client_id=YOUR_CLIENT_ID&
redirect_uri=https://your-app.com/callback&
scope=openid profile email&
state=random_state_string用户将看到授权页面,显示你的应用信息和请求的权限。用户确认授权后,浏览器将重定向到你的回调地址:
https://your-app.com/callback?code=AUTHORIZATION_CODE&state=random_state_string3. 交换 Token
使用授权码向 Token 端点请求令牌。支持 application/x-www-form-urlencoded(OAuth 2.0 标准格式)和 application/json 两种请求格式。
POST https://api.starecho.net/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=AUTHORIZATION_CODE&redirect_uri=https%3A%2F%2Fyour-app.com%2Fcallback&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET也可以使用 JSON 格式(Content-Type: application/json):
{
"grant_type": "authorization_code",
"code": "AUTHORIZATION_CODE",
"redirect_uri": "https://your-app.com/callback",
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET"
}TIP
大多数 OAuth 2.0 客户端库默认使用 application/x-www-form-urlencoded 格式,如果你使用的框架自动处理 Token 交换,通常无需手动指定 Content-Type。
也支持 HTTP Basic 认证(client_secret_basic),此时无需在请求体中传递 client_id 和 client_secret:
POST https://api.starecho.net/oauth/token
Authorization: Basic base64(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=AUTHORIZATION_CODE&redirect_uri=https%3A%2F%2Fyour-app.com%2Fcallback成功响应:
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"id_token": "eyJhbGciOiJSUzI1NiIs...",
"refresh_token": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4...",
"scope": "openid profile email"
}| 字段 | 说明 |
|---|---|
access_token | 访问令牌(RS256 JWT,有效期 1 小时) |
token_type | 固定为 Bearer |
expires_in | 令牌有效期(秒) |
id_token | 身份令牌(仅当 scope 包含 openid 时返回) |
refresh_token | 刷新令牌(有效期 30 天,每次使用后轮换) |
scope | 实际授权的权限范围 |
4. 获取用户信息
使用 access_token 请求用户信息:
GET https://api.starecho.net/oauth/userinfo
Authorization: Bearer ACCESS_TOKEN成功响应:
{
"sub": "100001",
"name": "StarUser",
"email": "user@example.com",
"gender": "male"
}| 字段 | Scope | 说明 |
|---|---|---|
sub | 始终返回 | 用户唯一标识(uid) |
name | profile | 用户昵称 |
gender | profile | 性别(male / female) |
email | email | 邮箱地址 |
5. 刷新令牌
当 access_token 过期后,你可以使用 refresh_token 获取一组新的令牌,而无需用户重新授权。这遵循 OIDC Core 1.0 §12 规范。
POST https://api.starecho.net/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=YOUR_REFRESH_TOKEN&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET也可以使用 JSON 格式:
{
"grant_type": "refresh_token",
"refresh_token": "YOUR_REFRESH_TOKEN",
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET"
}PKCE 应用
如果你的应用使用 PKCE 认证(无 client_secret),只需传递 client_id 即可,无需 client_secret。
请求参数:
| 参数 | 必填 | 说明 |
|---|---|---|
grant_type | ✅ | 固定为 refresh_token |
refresh_token | ✅ | 之前获取的刷新令牌 |
client_id | ✅ | 你的应用 Client ID |
client_secret | 条件必填 | 非 PKCE 应用必须提供 |
scope | 可选 | 可以请求原始 scope 的子集(缩小权限范围) |
成功响应:
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"id_token": "eyJhbGciOiJSUzI1NiIs...",
"refresh_token": "NEW_REFRESH_TOKEN",
"scope": "openid profile email"
}令牌轮换
每次使用 refresh_token 后,服务器将返回一个新的 refresh_token,原 refresh_token 立即失效。请务必保存并使用最新的 refresh_token。
如果检测到已失效的 refresh_token 被再次使用(疑似令牌泄露),服务器将自动撤销该用户在该应用下的所有刷新令牌,以保护账号安全。
刷新令牌属性:
| 属性 | 值 |
|---|---|
| 有效期 | 30 天 |
| 轮换策略 | 每次使用后签发新令牌,旧令牌失效 |
| 重放检测 | 检测到旧令牌重用时,撤销该应用的所有令牌 |
| Scope 缩小 | 可在刷新时请求原始 scope 的子集 |
PKCE 支持
对于无法安全存储 client_secret 的客户端(如原生应用、SPA),可以使用 PKCE(Proof Key for Code Exchange) 代替客户端密钥进行认证。
流程
- 生成 code_verifier:43-128 位的随机字符串
- 计算 code_challenge:
BASE64URL(SHA256(code_verifier)) - 发起授权时携带
code_challenge和code_challenge_method=S256 - 交换 Token 时携带
code_verifier(不需要client_secret)
Token 请求示例(PKCE):
{
"grant_type": "authorization_code",
"code": "AUTHORIZATION_CODE",
"redirect_uri": "https://your-app.com/callback",
"client_id": "YOUR_CLIENT_ID",
"code_verifier": "YOUR_CODE_VERIFIER"
}原生应用接入示例
桌面端(本地回环 + PKCE):
// 1. 在本地启动 HTTP 服务器监听随机端口
// 2. 构造授权 URL
https://starecho.net/oauth/authorize?
response_type=code&
client_id=YOUR_CLIENT_ID&
redirect_uri=http://127.0.0.1:12345/callback&
scope=openid profile email&
state=random_state&
code_challenge=CHALLENGE&
code_challenge_method=S256
// 3. 打开系统浏览器让用户授权
// 4. 用户授权后浏览器跳转到 http://127.0.0.1:12345/callback?code=xxx
// 5. 本地服务器接收授权码,用 PKCE 交换 Token移动端(自定义 URL Scheme + PKCE):
// 前提:在应用配置中注册了 scheme“starecho”
https://starecho.net/oauth/authorize?
response_type=code&
client_id=YOUR_CLIENT_ID&
redirect_uri=starecho://callback&
scope=openid profile email&
state=random_state&
code_challenge=CHALLENGE&
code_challenge_method=S256
// 用户授权后系统通过 Deep Link 唤起应用:
// starecho://callback?code=AUTHORIZATION_CODE&state=random_stateScope 说明
| Scope | 包含的信息 |
|---|---|
openid | 返回 id_token,包含 sub(用户 ID) |
profile | 用户昵称(name)、性别(gender) |
email | 用户邮箱(email) |
ID Token
当请求的 scope 包含 openid 时,Token 响应中将包含 id_token。这是一个 RS256 签名的 JWT,包含以下 Claims:
| Claim | 说明 |
|---|---|
sub | 用户唯一标识(uid) |
aud | 你的 Client ID |
iss | 签发者 URL |
iat | 签发时间(Unix 时间戳) |
exp | 过期时间(Unix 时间戳) |
nonce | 请求中传入的 nonce(如有) |
name | 用户昵称(scope 含 profile 时) |
email | 邮箱地址(scope 含 email 时) |
gender | 性别(scope 含 profile 时) |
可通过 JWKS 端点 获取公钥来验证 id_token 的签名。
端点参考
| 端点 | 方法 | URL | 说明 |
|---|---|---|---|
| Discovery | GET | /.well-known/openid-configuration | OIDC 服务发现 |
| JWKS | GET | /oauth/jwks | JSON Web Key Set 公钥 |
| 授权 | GET | /oauth/authorize | 授权页面(前端) |
| 授权检查 | GET | /oauth/authorize/check | 验证授权参数 |
| Token | POST | /oauth/token | 授权码换取令牌 / 刷新令牌 |
| UserInfo | GET | /oauth/userinfo | 获取用户信息 |
基础 URL
- 前端页面:
https://starecho.net - API 端点:
https://api.starecho.net
错误处理
授权错误
当授权失败时,用户将被重定向到回调地址并附带错误参数:
https://your-app.com/callback?error=access_denied&state=random_state_string| 错误码 | 说明 |
|---|---|
access_denied | 用户拒绝授权 |
invalid_client | 应用不存在 |
client_not_approved | 应用未通过审核 |
invalid_redirect_uri | 回调地址不匹配 |
Token 错误
Token 端点返回 JSON 格式的错误:
{
"error": "invalid_grant",
"error_description": "Authorization code expired"
}| 错误码 | 说明 |
|---|---|
unsupported_grant_type | 不支持的授权类型 |
invalid_grant | 授权码无效、已过期或已使用;或刷新令牌无效、已过期或已被撤销 |
invalid_client | Client ID 或 Secret 错误 |
invalid_scope | 请求的 scope 超出原始授权范围(仅刷新令牌) |
安全建议
- 始终使用 HTTPS — Web 应用的回调地址必须使用
https://协议 - 验证 state — 对比授权请求和回调中的
state参数,防止 CSRF 攻击 - 妥善保管 client_secret — 切勿在前端代码、版本控制或日志中暴露密钥
- 使用 PKCE — 原生应用、SPA 等无法安全存储密钥的应用应使用 PKCE,强烈建议所有原生应用都启用 PKCE
- 验证 id_token — 校验签名、issuer、audience 和过期时间
- 最小权限原则 — 仅请求所需的 scope
原生应用回调地址说明
| 平台 | 回调地址格式 | 说明 |
|---|---|---|
| Web 应用 | https://your-app.com/callback | 需在应用配置中注册 |
| 桌面端 | http://127.0.0.1:{port}/callback | 自动允许,端口 1024-65535 |
| 桌面端 | http://localhost:{port}/callback | 自动允许,端口 1024-65535 |
| 移动端 | {scheme}://callback | 需在应用配置中注册 scheme |
示例代码
JavaScript / Node.js
// 1. 构造授权 URL 并重定向
const params = new URLSearchParams({
response_type: 'code',
client_id: 'YOUR_CLIENT_ID',
redirect_uri: 'https://your-app.com/callback',
scope: 'openid profile email',
state: crypto.randomUUID(),
})
window.location.href = `https://starecho.net/oauth/authorize?${params}`
// 2. 回调处理:用授权码换取 Token
const tokenRes = await fetch('https://api.starecho.net/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'authorization_code',
code: authorizationCode,
redirect_uri: 'https://your-app.com/callback',
client_id: 'YOUR_CLIENT_ID',
client_secret: 'YOUR_CLIENT_SECRET',
}),
})
const tokens = await tokenRes.json()
// 3. 获取用户信息
const userRes = await fetch('https://api.starecho.net/oauth/userinfo', {
headers: { Authorization: `Bearer ${tokens.access_token}` },
})
const user = await userRes.json()
console.log(user.name, user.email)Python
import requests
# 交换 Token
token_res = requests.post('https://api.starecho.net/oauth/token', json={
'grant_type': 'authorization_code',
'code': authorization_code,
'redirect_uri': 'https://your-app.com/callback',
'client_id': 'YOUR_CLIENT_ID',
'client_secret': 'YOUR_CLIENT_SECRET',
})
tokens = token_res.json()
# 获取用户信息
user_res = requests.get('https://api.starecho.net/oauth/userinfo', headers={
'Authorization': f'Bearer {tokens["access_token"]}'
})
user = user_res.json()
print(user['name'], user['email'])