跳转至

App 嵌入式 H5 免登接入文档

基于 appid / appkey 的 App 内嵌 H5 静默登录(SSO)对接方案。

本文档面向第三方技术团队,描述如何将我方 H5 业务页面嵌入贵方 App,并在用户已登录贵方 App 的前提下,实现到我方 H5 的无感登录。

约定:下文中 接入方 指贵公司(App 与 App Server 侧),平台方 指我方(H5 与我方 Server 侧)。

1. 方案概述

本方案采用 后端预换取授权码(AuthCode) 的模式。此模式安全性最高,appkey 仅存储在接入方 Server 端,完全不暴露给 App 客户端或 H5。

核心流程:

  1. 接入方 App 请求自己的后端获取 H5 跳转链接;
  2. 接入方 Server 用 appkey 签名后,调用平台方接口换取一次性 AuthCode;
  3. 接入方 App 用带 AuthCode 的 URL 打开 WebView 加载我方 H5;
  4. 我方 H5 用 AuthCode 静默换取平台方 AccessToken,完成登录。

2. 核心角色定义

角色 说明
接入方 App(客户端) 用户已登录,持有接入方 Session
接入方 Server 持有平台方颁发的 appid 与 appkey,作为可信身份提供方
平台方 H5 嵌入在 App WebView 中的业务页面
平台方 Server 资源拥有者,维护用户表,负责校验签名、签发与兑换 AuthCode、发放 AccessToken

3. 准备工作

  1. 平台方为接入方分配唯一的 appid(应用标识)与 appkey(签名秘钥);
  2. appkey 必须严格保存在接入方 Server 端,严禁下发到 App 客户端或 H5;
  3. 双方约定一个用于「Token 过期后重新授权」的跳转地址(详见第 9 节),该地址在平台方应用配置中登记。

4. 交互时序图

sequenceDiagram
    participant User as 用户
    participant App as 接入方 App
    participant AServer as 接入方 Server
    participant MyServer as 平台方 Server
    participant H5 as 平台方 H5

    Note right of User: 用户在接入方 App 内已登录

    User->>App: 点击进入“平台方 H5 业务”

    Note right of App: 步骤1:请求免登入口
    App->>AServer: 1. 请求获取 H5 跳转链接 (携带当前用户 ID)

    Note right of AServer: 步骤2:服务端身份握手 (核心鉴权)
    AServer->>AServer: 使用 appkey 生成签名 (sign)
    AServer->>MyServer: 2. 调用“获取授权码”接口<br/>(appid, sign, ts, nonce, tp_uid, tp_data)

    Note right of MyServer: 校验签名合法性<br/>记录 tp_uid<br/>创建/更新用户<br/>生成一次性 AuthCode (有效期 5 分钟)
    MyServer-->>AServer: 3. 返回 AuthCode

    Note over AServer, App: 步骤3:返回带票据的 URL
    AServer-->>App: 4. 返回完整 URL: https://h5...?authCode=...

    Note over App, H5: 步骤4:打开页面
    App->>H5: 5. WebView 加载该 URL

    Note over H5, MyServer: 步骤5:兑换 AccessToken (静默登录)
    H5->>H5: 解析 URL 中的 authCode 参数
    H5->>MyServer: 6. 提交 AuthCode 登录 (POST /lemon/ss_oauth/verify_code)

    Note right of MyServer: 校验 AuthCode 有效性 (一次性)<br/>取出绑定的用户<br/>签发平台方 AccessToken
    MyServer-->>H5: 7. 返回 AccessToken & 用户信息

    H5->>H5: 存储 AccessToken (localStorage / cookie)
    H5-->>User: 8. 展示业务页面 (已登录状态)

5. 详细步骤解析

第一阶段:获取授权码(Server-to-Server)

发生在用户点击入口的瞬间,接入方 App 客户端无需感知 appid/appkey。

  1. App 发起请求:App 告知接入方 Server「当前用户需访问 H5」。
  2. 接入方 Server 签名并请求:
  3. 准备参数:appid、ts(秒级时间戳)、nonce(随机串)、tp_uid(接入方用户唯一 ID)、tp_data(用户基础信息:昵称、头像、手机号);
  4. 用 appkey 计算 sign;
  5. 调用平台方接口:POST /lemon/ss_oauth/generate_code(详见第 6 节 接口 A)。
  6. 平台方 Server 校验与发码:
  7. 根据 appid 查到对应 appkey;
  8. 重新计算签名,比对 sign;
  9. 校验通过后,将 tp_uid、tp_data 与一个一次性 AuthCode 绑定,写入临时缓存(有效期 5 分钟);
  10. 返回 AuthCode 给接入方 Server。

第二阶段:页面跳转与登录(Client-Side)

  1. 拼接 URL:接入方 Server 收到 AuthCode 后,拼接成 H5 地址,如 https://h5.{平台域名}/index?authCode={AuthCode},返回给 App。
  2. App 打开 H5:App 使用 WebView 加载该 URL。
  3. H5 兑换 Token:
  4. H5 初始化时检查 URL 是否含 authCode 参数;
  5. 若有,立即调用平台方接口 POST /lemon/ss_oauth/verify_code 提交 AuthCode(详见第 6 节 接口 B)。
  6. 平台方 Server 完成登录:
  7. 校验 AuthCode 是否存在且未过期(一次性,校验后即销毁,防重放);
  8. 取出绑定的用户信息;
  9. 账号映射:查询平台方用户表是否已绑定该 tp_uid:
    • 若已存在:更新昵称/头像/手机号,直接签发 AccessToken;
    • 若不存在:使用 tp_data 自动注册新用户并建立绑定,再签发 AccessToken;
  10. 返回 AccessToken 及用户信息给 H5。

6. 关键接口定义

接口 A:获取授权码(Server-to-Server)

  • 调用方:接入方 Server → 平台方 Server
  • 路径:POST /lemon/ss_oauth/generate_code
  • Content-Type:application/json

请求参数

字段 类型 必填 约束 说明
appid String 是 仅字母数字,长度 8–32 平台方分配的应用标识
ts Integer (int64) 是 Unix 时间戳(秒) 用于防重放,请使用服务器当前时间
nonce String 是 长度 4–32 随机字符串,每次请求唯一
tp_uid String 是 仅字母数字,长度 4–32 接入方用户的唯一标识
tp_data Object 是 — 用户基础信息,见下表
sign String 是 仅字母数字,长度 8–40 签名,计算方式见「sign 签名算法」

tp_data 字段(用户基础信息)

字段 类型 必填 约束 说明
nickname String 否 — 用户昵称,用于自动注册时填充资料
phone String 否 — 手机号,用于账号关联
avatar String 否 若填写必须为合法 URL 用户头像地址

说明:tp_data 为 JSON 对象(不是字符串)。三个子字段均可选,但建议至少传入 nickname,以保证自动注册的用户有可展示的资料。

sign 签名算法

参与签名的字段(共 5 个,注意不包含 tp_data):

  • appid
  • appkey(平台方分配的密钥,仅参与签名计算,不放入请求体)
  • nonce
  • tp_uid
  • ts

计算步骤:

  1. 将上述 5 个参数按 参数名升序 排序;
  2. 以 key=value 形式用 & 连接,得到待签名字符串(形如 a=xxx&b=xxx&c=xxx);
  3. 对该字符串计算 MD5,取 小写十六进制 结果即为 sign。

注意:待签名字符串中各 value 需按 URL Query 规则编码(同 Go url.Values.Encode() 的行为)。由于参与签名的字段均为 ASCII 字符(不含中文),实际编码后通常无变化。tp_data 中的中文昵称等 不参与签名,故无需关心其编码。

请求示例

curl -X POST -H "Content-Type: application/json" \
  https://api.{平台域名}/lemon/ss_oauth/generate_code \
  -d '{
    "appid": "aabbccdd",
    "ts": 191920203030,
    "nonce": "29f023be",
    "tp_uid": "11223344",
    "tp_data": {
      "nickname": "abc",
      "phone": "13800000000",
      "avatar": "https://some.url/pic.jpg"
    },
    "sign": "ba5c7916f2b1d29c3421645c88ca8039"
  }'

签名计算示例(对照上例)

待签名参数(含 appkey,按参数名升序):

参数名 参数值
appid aabbccdd
appkey aabbddcc
nonce 29f023be
tp_uid 11223344
ts 191920203030

待签名字符串:

appid=aabbccdd&appkey=aabbddcc&nonce=29f023be&tp_uid=11223344&ts=191920203030

MD5 结果(小写):

ba5c7916f2b1d29c3421645c88ca8039

响应

成功响应(HTTP 200):

{
  "Code": 0,
  "Message": "Success",
  "Error": "",
  "Data": {
    "auth_code": "c-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  }
}
字段 类型 说明
Code Integer 0 表示成功,非 0 表示失败
Message String 状态描述
Error String 错误详情(成功时为空)
Data.auth_code String 一次性授权码,有效期 5 分钟,仅可使用一次

失败响应示例:

{
  "Code": 1001,
  "Message": "sign not match",
  "Error": "sign not match",
  "Data": null
}

常见失败原因:appid 不存在、sign 校验失败、ts 偏离服务器时间过多、必填字段缺失或不符合约束。

接口 B:授权码登录(H5 → Server)

  • 调用方:平台方 H5 → 平台方 Server
  • 路径:POST /lemon/ss_oauth/verify_code
  • Content-Type:application/json

请求参数

字段 类型 必填 约束 说明
AuthCode String 是 长度 4–60 接口 A 返回的 auth_code

请求示例:

curl -X POST -H "Content-Type: application/json" \
  https://api.{平台域名}/lemon/ss_oauth/verify_code \
  -d '{
    "AuthCode": "c-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  }'

响应

成功响应(HTTP 200):

{
  "Code": 0,
  "Message": "Success",
  "Error": "",
  "Data": {
    "Token": "eyJhbGciOi...",
    "ExpiresAt": 1919202090,
    "UserId": 20000123,
    "Username": "th_11223344",
    "Nickname": "abc",
    "OpenID": "o_xxxxxxxx",
    "Avatar": "https://some.url/pic.jpg",
    "IsGuest": false,
    "RoomId": 100000
  }
}

主要字段说明:

字段 类型 说明
Token String 平台方访问令牌,后续业务接口需携带
ExpiresAt Integer (int64) Token 过期时间(Unix 秒)
UserId Integer 平台方用户 ID
Username String 平台方用户名(自动生成的格式为 th_{tp_uid})
Nickname String 昵称(取自 tp_data.nickname)
OpenID String 平台方 OpenID,可用于跨端识别同一用户
Avatar String 头像地址(取自 tp_data.avatar)
IsGuest Boolean 是否为访客模式
RoomId Integer 默认可进入的房间 ID

H5 获取 Token 后,应存储于 localStorage 或 cookie,并在后续所有业务请求中按平台方要求携带该 Token。

7. 账号映射说明

  • 平台方以 appid 对应的应用 + tp_uid 作为用户唯一性依据;
  • 首次调用时,平台方会使用 tp_data 自动注册一个新用户并建立绑定关系;
  • 后续同一 tp_uid 再次调用时,平台方会更新该用户的昵称/头像/手机号(若传入新值),并复用已有账号;
  • AuthCode 为一次性票据,校验后立即销毁,不可重复使用。

8. 安全性考量

  1. AppKey 不泄露:签名过程完全在接入方后端完成,App 端与网络传输中均不含 appkey,极大降低泄露风险。
  2. AuthCode 一次性:授权码仅可使用一次,校验后即销毁,且有效期短(5 分钟),防止被截获后重放。
  3. 签名防篡改与防重放:签名包含 ts 与 nonce,可防止请求被恶意修改或重放。建议接入方校验 ts 与服务器当前时间偏差不超过合理范围(如 ±5 分钟)。
  4. HTTPS:所有接口必须通过 HTTPS 调用。

9. Token 过期与重新授权

当 App 长时间切到后台再切回,可能出现 H5 的 AccessToken 过期的场景。处理方式如下:

  1. H5 在检测到 Token 失效时,会自动跳转至双方约定的「重新授权地址」(在平台方应用配置中登记的 token_auth_url,须为合法 URL,例如 yourapp://re-initial-oauth);
  2. 接入方 App 需在系统层注册并拦截该 URL Scheme / Deep Link,重新触发第 4 节的完整授权流程,获取新的带 AuthCode 的 URL,并重新加载 H5。

该跳转地址需在接入前与平台方确认并配置,App 端开发人员需负责处理对应唤起逻辑。

附录:签名计算参考实现(Go)

package main

import (
    "crypto/md5"
    "encoding/hex"
    "net/url"
    "strconv"
)

// SignData 计算签名 sign
// 入参 appkey 为平台方分配的密钥,严禁随请求体外发
func SignData(appid, appkey, tpUid, nonce string, ts int64) string {
    v := url.Values{}
    v.Add("appid", appid)
    v.Add("appkey", appkey)
    v.Add("ts", strconv.FormatInt(ts, 10))
    v.Add("tp_uid", tpUid)
    v.Add("nonce", nonce)
    // url.Values.Encode() 会自动按参数名升序排序并做 URL 编码
    s := v.Encode()
    m := md5.New()
    m.Write([]byte(s))
    return hex.EncodeToString(m.Sum(nil)) // 小写十六进制 MD5
}

若使用其它语言实现,请确保:参数按名升序、以 key=value 用 & 连接、最后做小写 MD5。建议用上述相同示例值自测,结果应为 ba5c7916f2b1d29c3421645c88ca8039。