Skip to content

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 持有平台方颁发的 appidappkey,作为可信身份提供方
平台方 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. 准备参数:appidts(秒级时间戳)、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_uidtp_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-Typeapplication/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-Typeapplication/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. 签名防篡改与防重放:签名包含 tsnonce,可防止请求被恶意修改或重放。建议接入方校验 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