注册与授权(绑定)
用户通过 PalmxApp 扫描网页二维码,将 Palm 账号与您系统中的用户账号建立关联。
架构说明
SDK 采用 iframe + postMessage 架构,彻底避免跨域问题:
┌─ 客户网站 (www.customer.com) ─────────────────────────┐
│ │
│ <script src="palm-oauth.js"> │
│ ┌─ iframe (open.palmoa.youtu.qq.com/palm/x/h5/oauth/qr/page) ──┐│
│ │ ││
│ │ [二维码] ← 同域请求,无跨域 ││
│ │ "请使用 PalmxApp 扫码绑定" ││
│ │ ││
│ │ 轮询状态... ││
│ │ 绑定成功 → postMessage({code, state}) ────────── ││──→ SDK 接收
│ └────────────────────────────────────────────────────┘│
│ │
│ SDK 将 { code, codeVerifier, state } 通过 Promise │
│ 返回给调用者,调用者自行发送给自己的后端完成绑定 │
│ │
└────────────────────────────────────────────────────────┘
快速开始
1. 引入 SDK
<script src="https://open.palmoa.youtu.qq.com/sdk/palm-oauth.js"></script>
就这一行,无需引入二维码库(二维码在 iframe 内渲染)。
2. 使用
<div id="bindContainer"></div>
<script>
PalmOAuth.init({
appId: 'your_app_id',
});
PalmOAuth.bind('#bindContainer')
.then(function (result) {
// result = { code, codeVerifier, state }
// 将结果发送给您的后端,完成账号绑定
fetch('/api/bindPalm', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + userToken, // 您系统的登录态
},
body: JSON.stringify(result),
}).then(function () {
alert('绑定成功!');
});
})
.catch(function (err) {
console.error('绑定失败:', err.message);
});
</script>
配置项
PalmOAuth.init({
// 必填
appId: 'your_app_id',
// 可选
baseUrl: 'https://open.palmoa.youtu.qq.com', // 接口域名
width: 300, // iframe 宽度 px
height: 360, // iframe 高度 px
lang: 'zh-CN', // 语言:zh-CN / en
// 事件回调
onStateChange: function (status) {}, // pending / scanned / confirmed / denied / expired
onSuccess: function (result) {}, // result = { code, codeVerifier, state }
onError: function (err) {}, // 错误
});
API
| 方法 | 说明 |
|---|---|
PalmOAuth.init(options) | 初始化配置 |
PalmOAuth.bind(container) | 在指定容器内展示二维码,发起绑定流程,返回 Promise |
PalmOAuth.cancel() | 取消当前绑定流程,移除 iframe |
PalmOAuth.destroy() | 销毁实例 |
后端对接
SDK 绑定成功后,Promise resolve 返回以下数据:
{
code: "f30cb9df-dc28-4a28-858e-0eb7c6f59c67", // 授权码
codeVerifier: "P25ffL26KVLZozkFdAQ2yxRZADK9Y88NDu8WlIYhdA", // PKCE 密钥
state: "state_1700000000_abc123" // 防 CSRF 随机串
}
您的前端拿到后,发送给自己的后端(必须带上您系统的用户登录态,以便后端知道是哪个用户要绑定):
PalmOAuth.bind('#container').then(function (result) {
fetch('/api/bindPalm', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + userToken, // 您系统的登录态(必须)
},
body: JSON.stringify({
code: result.code,
code_verifier: result.codeVerifier,
}),
});
});
您的后端收到后,调用 Palm Token 接口换取 open_id,并与当前登录用户建立绑定关系:
// POST /api/bindPalm
func BindPalmHandler(w http.ResponseWriter, r *http.Request) {
// 1. 验证当前登录用户(从您自己的登录态中获取)
currentUser := GetCurrentUser(r)
// 2. 解析请求体
var req struct {
Code string `json:"code"`
CodeVerifier string `json:"code_verifier"`
}
json.NewDecoder(r.Body).Decode(&req)
// 3. 用 code 换取 Palm open_id
body, _ := json.Marshal(map[string]string{
"GrantType": "authorization_code",
"Code": req.Code,
"CodeVerifier": req.CodeVerifier,
})
httpReq, _ := http.NewRequest("POST",
"https://open.palmoa.youtu.qq.com/palm/openai/x/oauth/token",
bytes.NewReader(body))
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+API_KEY)
resp, _ := http.DefaultClient.Do(httpReq)
defer resp.Body.Close()
var result struct {
Data struct {
OpenId string `json:"OpenId"`
AccessToken string `json:"AccessToken"`
RefreshToken string `json:"RefreshToken"`
} `json:"data"`
}
json.NewDecoder(resp.Body).Decode(&result)
// 4. 绑定 & 保存 token
db.BindPalmAccount(currentUser.ID, result.Data.OpenId)
db.SavePalmTokens(currentUser.ID, result.Data.AccessToken, result.Data.RefreshToken)
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true, "message": "绑定成功",
})
}
完整 HTML 示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>绑定 Palm 账号</title>
<style>
body {
display: flex; justify-content: center; align-items: center;
min-height: 100vh; margin: 0;
font-family: -apple-system, sans-serif; background: #f5f7fa;
}
.bind-card {
background: #fff; border-radius: 12px; padding: 32px;
box-shadow: 0 2px 12px rgba(0,0,0,0.08); text-align: center;
}
.bind-card h2 { margin: 0 0 8px; color: #333; }
.bind-card p { margin: 0 0 20px; color: #666; font-size: 14px; }
</style>
</head>
<body>
<div class="bind-card">
<h2>绑定 Palm 账号</h2>
<p>使用 PalmxApp 扫描下方二维码,将您的 Palm 账号与本站账号绑定</p>
<div id="bindContainer"></div>
</div>
<script src="https://open.palmoa.youtu.qq.com/sdk/palm-oauth.js"></script>
<script>
PalmOAuth.init({
appId: 'your_app_id',
onStateChange: function (s) { console.log('状态:', s); },
});
PalmOAuth.bind('#bindContainer')
.then(function (result) {
// 注意:后端期望的字段名是 code_verifier(下划线),而 SDK 返回的是 codeVerifier(驼峰)
fetch('/api/bindPalm', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code: result.code,
code_verifier: result.codeVerifier,
}),
}).then(function () {
alert('绑定成功!');
window.location.reload();
});
})
.catch(function (e) { alert('绑定失败: ' + e.message); });
</script>
</body>
</html>