App 嵌入式 H5 免登接入文档
基于 appid / appkey 的 App 内嵌 H5 静默登录(SSO)对接方案。
本文档面向第三方技术团队,描述如何将我方 H5 业务页面嵌入贵方 App,并在用户已登录贵方 App 的前提下,实现到我方 H5 的无感登录。
约定:下文中 接入方 指贵公司(App 与 App Server 侧),平台方 指我方(H5 与我方 Server 侧)。
1. 方案概述
本方案采用 后端预换取授权码(AuthCode) 的模式。此模式安全性最高,appkey 仅存储在接入方 Server 端,完全不暴露给 App 客户端或 H5。
核心流程:
- 接入方 App 请求自己的后端获取 H5 跳转链接;
- 接入方 Server 用
appkey签名后,调用平台方接口换取一次性AuthCode; - 接入方 App 用带
AuthCode的 URL 打开 WebView 加载我方 H5; - 我方 H5 用
AuthCode静默换取平台方AccessToken,完成登录。
2. 核心角色定义
| 角色 | 说明 |
|---|---|
| 接入方 App(客户端) | 用户已登录,持有接入方 Session |
| 接入方 Server | 持有平台方颁发的 appid 与 appkey,作为可信身份提供方 |
| 平台方 H5 | 嵌入在 App WebView 中的业务页面 |
| 平台方 Server | 资源拥有者,维护用户表,负责校验签名、签发与兑换 AuthCode、发放 AccessToken |
3. 准备工作
- 平台方为接入方分配唯一的
appid(应用标识)与appkey(签名秘钥); appkey必须严格保存在接入方 Server 端,严禁下发到 App 客户端或 H5;- 双方约定一个用于「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。
- App 发起请求:App 告知接入方 Server「当前用户需访问 H5」。
- 接入方 Server 签名并请求:
- 准备参数:
appid、ts(秒级时间戳)、nonce(随机串)、tp_uid(接入方用户唯一 ID)、tp_data(用户基础信息:昵称、头像、手机号); - 用
appkey计算sign; - 调用平台方接口:
POST /lemon/ss_oauth/generate_code(详见第 6 节 接口 A)。 - 平台方 Server 校验与发码:
- 根据
appid查到对应appkey; - 重新计算签名,比对
sign; - 校验通过后,将
tp_uid、tp_data与一个一次性AuthCode绑定,写入临时缓存(有效期 5 分钟); - 返回
AuthCode给接入方 Server。
第二阶段:页面跳转与登录(Client-Side)
- 拼接 URL:接入方 Server 收到
AuthCode后,拼接成 H5 地址,如https://h5.{平台域名}/index?authCode={AuthCode},返回给 App。 - App 打开 H5:App 使用 WebView 加载该 URL。
- H5 兑换 Token:
- H5 初始化时检查 URL 是否含
authCode参数; - 若有,立即调用平台方接口
POST /lemon/ss_oauth/verify_code提交AuthCode(详见第 6 节 接口 B)。 - 平台方 Server 完成登录:
- 校验
AuthCode是否存在且未过期(一次性,校验后即销毁,防重放); - 取出绑定的用户信息;
- 账号映射:查询平台方用户表是否已绑定该
tp_uid:- 若已存在:更新昵称/头像/手机号,直接签发
AccessToken; - 若不存在:使用
tp_data自动注册新用户并建立绑定,再签发AccessToken;
- 若已存在:更新昵称/头像/手机号,直接签发
- 返回
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):
appidappkey(平台方分配的密钥,仅参与签名计算,不放入请求体)noncetp_uidts
计算步骤:
- 将上述 5 个参数按 参数名升序 排序;
- 以
key=value形式用&连接,得到待签名字符串(形如a=xxx&b=xxx&c=xxx); - 对该字符串计算 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. 安全性考量
- AppKey 不泄露:签名过程完全在接入方后端完成,App 端与网络传输中均不含
appkey,极大降低泄露风险。 - AuthCode 一次性:授权码仅可使用一次,校验后即销毁,且有效期短(5 分钟),防止被截获后重放。
- 签名防篡改与防重放:签名包含
ts与nonce,可防止请求被恶意修改或重放。建议接入方校验ts与服务器当前时间偏差不超过合理范围(如 ±5 分钟)。 - HTTPS:所有接口必须通过 HTTPS 调用。
9. Token 过期与重新授权
当 App 长时间切到后台再切回,可能出现 H5 的 AccessToken 过期的场景。处理方式如下:
- H5 在检测到 Token 失效时,会自动跳转至双方约定的「重新授权地址」(在平台方应用配置中登记的
token_auth_url,须为合法 URL,例如yourapp://re-initial-oauth); - 接入方 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。