高危操作核验
当用户在您系统中发起敏感操作(如转账、删除账号、修改密码等)时,可通过 Palm 核验功能要求用户在 PalmxApp 上进行掌纹二次确认。
流程概览
┌─ 您的前端 ──────────────────────────────────────────────────┐
│ │
│ 用户发起危险操作 → 请求您的后端 │
│ 等待后端返回核验结果 → 展示给用户 │
│ │
└──────────────────────────────────────────────────────────────┘
│ ↑
↓ │
┌─ 您的后端 ──────────────────────────────────────────────────┐
│ │
│ 1. 检测到危险操作 → 调用 VerifyCreate 创建核验 challenge │
│ 2. 轮询 VerifyQuery 查询核验状态 │
│ 3. 状态为 approved → 放行操作,返回结果给前端 │
│ │
└──────────────────────────────────────────────────────────────┘
(Palm 推送通知)
↓
┌─ PalmxApp ──────────────────────────────────────────────────┐
│ │
│ 用户在手机上看到核验请求 → 掌纹确认 / 拒绝 │
│ │
└──────────────────────────────────────────────────────────────┘
Step 1: 后端创建核验 challenge
POST /palm/openai/x/oauth/verify/challenges/create
Authorization: Bearer <API_KEY>
Content-Type: application/json
{
"ChallengeId": "550e8400-e29b-41d4-a716-446655440000", // 您自己生成的 UUID,幂等键
"OpenId": "a1b2c3d4e5f6...", // 用户的 open_id(绑定时获取的)
"Action": "withdraw", // 业务动作标识
"ActionDetail": "{\"amount\":\"1000 USD\"}" // 业务详情 JSON(≤512字节,会展示给用户)
}
响应:
{
"code": 0,
"data": {
"ExpiresAt": 1700000120, // 核验过期时间戳(秒)
"Status": "pending" // 初始状态
}
}
幂等性:同一个
ChallengeId重复提交不会创建新的 challenge,而是返回已有的状态。
Step 2: 后端轮询核验结果
您的前端轮询您自己的后端接口,您的后端再调用 Palm VerifyQuery 接口查询状态:
POST /palm/openai/x/oauth/verify/challenges/query
Authorization: Bearer <API_KEY>
Content-Type: application/json
{
"ChallengeId": "550e8400-e29b-41d4-a716-446655440000"
}
响应:
{
"code": 0,
"data": {
"Status": "approved" // pending / approved / denied / expired
}
}
状态说明
| Status | 含义 | 前端动作 |
|---|---|---|
pending | 等待用户确认 | 继续轮询(建议 2s 间隔) |
approved | 用户已确认 | 停止轮询,通知后端放行操作 |
denied | 用户已拒绝 | 停止轮询,提示用户操作已被拒绝 |
expired | 超时未响应 | 停止轮询,提示超时,可让用户重试 |
完整后端示例(Go)
// POST /api/withdraw — 发起提现,创建核验 challenge
func WithdrawHandler(w http.ResponseWriter, r *http.Request) {
bizId := GetCurrentBizId(r) // 您系统中的业务标识(如订单号、用户ID等)
openId := db.GetPalmOpenId(bizId)
// 创建核验 challenge
challengeId := uuid.New().String()
body, _ := json.Marshal(map[string]string{
"ChallengeId": challengeId,
"OpenId": openId,
"Action": "withdraw",
"ActionDetail": fmt.Sprintf(`{"amount":"%s","to":"%s"}`, req.Amount, req.To),
})
httpReq, _ := http.NewRequest("POST",
"https://open.palmoa.youtu.qq.com/palm/openai/x/oauth/verify/challenges/create",
bytes.NewReader(body))
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+API_KEY)
http.DefaultClient.Do(httpReq)
// 保存 challenge 与业务的关联,返回前端需要的信息
db.SavePendingWithdraw(bizId, challengeId, req.Amount, req.To)
json.NewEncoder(w).Encode(map[string]interface{}{"success": true})
}
// POST /api/withdraw/poll — 前端轮询核验状态
func WithdrawPollHandler(w http.ResponseWriter, r *http.Request) {
bizId := GetCurrentBizId(r)
challengeId := db.GetPendingChallenge(bizId)
// 调用 Palm VerifyQuery
body, _ := json.Marshal(map[string]string{"ChallengeId": challengeId})
httpReq, _ := http.NewRequest("POST",
"https://open.palmoa.youtu.qq.com/palm/openai/x/oauth/verify/challenges/query",
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 {
Status string `json:"Status"`
} `json:"data"`
}
json.NewDecoder(resp.Body).Decode(&result)
// approved → 执行业务逻辑
if result.Data.Status == "approved" {
executeWithdraw(bizId, challengeId)
}
// 返回您自己定义的业务状态给前端
json.NewEncoder(w).Encode(map[string]string{"status": result.Data.Status})
}